forge-workflow 0.0.8 → 0.0.10
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/commands/dev.md +2 -2
- package/.claude/commands/plan.md +2 -2
- package/.claude/commands/premerge.md +2 -2
- package/.claude/commands/review.md +5 -2
- package/.claude/commands/ship.md +6 -5
- package/.claude/commands/status.md +4 -4
- package/.claude/rules/greptile-review-process.md +4 -4
- package/.cline/workflows/dev.md +2 -2
- package/.cline/workflows/plan.md +2 -2
- package/.cline/workflows/premerge.md +2 -2
- package/.cline/workflows/review.md +5 -2
- package/.cline/workflows/ship.md +6 -5
- package/.cline/workflows/status.md +4 -4
- package/.codex/skills/dev/SKILL.md +2 -2
- package/.codex/skills/plan/SKILL.md +2 -2
- package/.codex/skills/premerge/SKILL.md +2 -2
- package/.codex/skills/review/SKILL.md +5 -2
- package/.codex/skills/ship/SKILL.md +6 -5
- package/.codex/skills/status/SKILL.md +4 -4
- package/.cursor/commands/dev.md +2 -2
- package/.cursor/commands/plan.md +2 -2
- package/.cursor/commands/premerge.md +2 -2
- package/.cursor/commands/review.md +5 -2
- package/.cursor/commands/ship.md +6 -5
- package/.cursor/commands/status.md +4 -4
- package/.github/prompts/dev.prompt.md +2 -2
- package/.github/prompts/plan.prompt.md +2 -2
- package/.github/prompts/premerge.prompt.md +2 -2
- package/.github/prompts/review.prompt.md +5 -2
- package/.github/prompts/ship.prompt.md +6 -5
- package/.github/prompts/status.prompt.md +4 -4
- package/.github/workflows/beads-to-github.yml +44 -11
- package/.github/workflows/github-to-beads.yml +11 -8
- package/.kilocode/workflows/dev.md +2 -2
- package/.kilocode/workflows/plan.md +2 -2
- package/.kilocode/workflows/premerge.md +2 -2
- package/.kilocode/workflows/review.md +5 -2
- package/.kilocode/workflows/ship.md +6 -5
- package/.kilocode/workflows/status.md +4 -4
- package/.opencode/commands/dev.md +2 -2
- package/.opencode/commands/plan.md +2 -2
- package/.opencode/commands/premerge.md +2 -2
- package/.opencode/commands/review.md +5 -2
- package/.opencode/commands/ship.md +6 -5
- package/.opencode/commands/status.md +4 -4
- package/.roo/commands/dev.md +2 -2
- package/.roo/commands/plan.md +2 -2
- package/.roo/commands/premerge.md +2 -2
- package/.roo/commands/review.md +5 -2
- package/.roo/commands/ship.md +6 -5
- package/.roo/commands/status.md +4 -4
- package/AGENTS.md +10 -9
- package/CLAUDE.md +12 -0
- package/README.md +12 -6
- package/bin/forge.js +24 -8
- package/docs/BEADS_GITHUB_SYNC.md +32 -2
- package/docs/EXAMPLES.md +22 -22
- package/docs/ROADMAP.md +3 -3
- package/docs/TOOLCHAIN.md +150 -160
- package/lib/agents/codex.plugin.json +3 -0
- package/lib/agents-config.js +18 -12
- package/lib/beads-bootstrap.js +225 -0
- package/lib/beads-health-check.js +55 -10
- package/lib/beads-setup.js +104 -28
- package/lib/beads-sync-scaffold.js +11 -6
- package/lib/codex-skills.js +54 -1
- package/lib/commands/_issue.js +11 -1
- package/lib/commands/issues.js +49 -0
- package/lib/commands/plan.js +5 -2
- package/lib/commands/recommend.js +22 -1
- package/lib/commands/setup.js +247 -27
- package/lib/commands/ship.js +188 -5
- package/lib/commands/status.js +187 -19
- package/lib/commands/team.js +11 -1
- package/lib/commands/test.js +125 -25
- package/lib/commands/validate.js +232 -9
- package/lib/commands/worktree.js +27 -54
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/detect-worktree.js +9 -10
- package/lib/forge-issues.js +326 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/lefthook-check.js +5 -2
- package/lib/project-memory.js +564 -0
- package/lib/runtime-health.js +100 -12
- package/lib/setup-action-log.js +2 -0
- package/lib/setup-summary-renderer.js +15 -11
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +177 -0
- package/lib/status/beads-snapshot.js +102 -0
- package/lib/status/presenter.js +65 -0
- package/lib/workflow/enforce-stage.js +14 -8
- package/lib/workflow/state-manager.js +349 -0
- package/package.json +12 -4
- package/scripts/beads-context.sh +124 -5
- package/scripts/beads-context.test.js +21 -4
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +263 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +45 -222
- package/scripts/forge-team/lib/hooks.sh +1 -1
- package/scripts/forge-team/lib/sync-github.sh +160 -28
- package/scripts/forge-team/lib/verify.sh +1 -1
- package/scripts/forge-team/lib/workload.sh +56 -27
- package/scripts/forge-team/tests/sync-github.test.sh +195 -58
- package/scripts/forge-team/tests/workload.test.sh +35 -4
- package/scripts/github-beads-sync/index.mjs +122 -98
- package/scripts/github-beads-sync/mapping.mjs +54 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
- package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
- package/scripts/github-beads-sync/run-bd.mjs +4 -2
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/preflight.sh +181 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +84 -330
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +39 -0
- package/scripts/test-ci-shard.js +244 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +335 -38
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
package/scripts/test.js
CHANGED
|
@@ -1,17 +1,70 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Cross-platform test runner
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Cross-platform test runner shared by the pre-push hook and local validation.
|
|
4
|
+
*
|
|
5
|
+
* It runs only the tests affected by known changes when possible and falls back
|
|
6
|
+
* to the full suite for package-level or unknown-file changes.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
const { spawnSync } = require('node:child_process');
|
|
9
|
+
const { execFileSync: defaultExecFileSync, spawnSync: defaultSpawnSync } = require('node:child_process');
|
|
9
10
|
const fs = require('node:fs');
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
const {
|
|
13
|
+
getAffectedTestFiles,
|
|
14
|
+
getChangedFiles,
|
|
15
|
+
} = require('../lib/commands/test');
|
|
16
|
+
|
|
17
|
+
const PACKAGE_LEVEL_PATHS = new Set([
|
|
18
|
+
'package.json',
|
|
19
|
+
'bun.lockb',
|
|
20
|
+
'pnpm-lock.yaml',
|
|
21
|
+
'yarn.lock',
|
|
22
|
+
'package-lock.json',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const KNOWN_TARGETABLE_PREFIXES = [
|
|
26
|
+
'.claude/commands/',
|
|
27
|
+
'.cursor/',
|
|
28
|
+
'.cline/',
|
|
29
|
+
'.roo/',
|
|
30
|
+
'.kilocode/',
|
|
31
|
+
'.opencode/',
|
|
32
|
+
'.codex/skills/',
|
|
33
|
+
'.forge/',
|
|
34
|
+
'.github/prompts/',
|
|
35
|
+
'.github/agentic-workflows/',
|
|
36
|
+
'.github/workflows/',
|
|
37
|
+
'test/',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const ALWAYS_RUN_RISK_TEST_TARGETS = [
|
|
41
|
+
// Windows + concurrent filesystem locking has failed post-merge; keep this
|
|
42
|
+
// in the fast PR lane until enough full-matrix runs prove it stable.
|
|
43
|
+
'test/project-memory.test.js',
|
|
44
|
+
];
|
|
45
|
+
|
|
12
46
|
const isWindows = process.platform === 'win32';
|
|
13
47
|
|
|
14
|
-
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} TestExecutionPlan
|
|
50
|
+
* @property {string[]} changedFiles
|
|
51
|
+
* @property {boolean} hasUnmappedFiles
|
|
52
|
+
* @property {boolean} hasUnknownChangedFiles
|
|
53
|
+
* @property {boolean} hasZeroResolvedTests
|
|
54
|
+
* @property {'targeted'|'full'} mode
|
|
55
|
+
* @property {string} reason
|
|
56
|
+
* @property {boolean} runE2E
|
|
57
|
+
* @property {boolean} runFullSuite
|
|
58
|
+
* @property {boolean} runTestEnv
|
|
59
|
+
* @property {boolean} runWorkflowTests
|
|
60
|
+
* @property {string[]} testTargets
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Detects the package manager to use for running test commands in this checkout.
|
|
65
|
+
*
|
|
66
|
+
* @returns {'bun'|'pnpm'|'yarn'|'npm'} The package manager inferred from lockfiles.
|
|
67
|
+
*/
|
|
15
68
|
function detectPackageManager() {
|
|
16
69
|
if (fs.existsSync('bun.lockb') || fs.existsSync('bun.lock')) return 'bun';
|
|
17
70
|
if (fs.existsSync('pnpm-lock.yaml')) return 'pnpm';
|
|
@@ -19,43 +72,287 @@ function detectPackageManager() {
|
|
|
19
72
|
return 'npm';
|
|
20
73
|
}
|
|
21
74
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Removes Git hook-only environment variables before spawning nested Git-aware commands.
|
|
77
|
+
*
|
|
78
|
+
* @param {NodeJS.ProcessEnv} [sourceEnv=process.env] Environment variables to sanitize.
|
|
79
|
+
* @returns {NodeJS.ProcessEnv} A copy of the environment without hook-specific Git variables.
|
|
80
|
+
*/
|
|
81
|
+
function stripGitHookEnv(sourceEnv = process.env) {
|
|
82
|
+
const env = { ...sourceEnv };
|
|
83
|
+
for (const key of Object.keys(env)) {
|
|
84
|
+
if (key === 'GIT_DIR' || key === 'GIT_WORK_TREE' || key === 'GIT_INDEX_FILE'
|
|
85
|
+
|| key === 'GIT_OBJECT_DIRECTORY' || key === 'GIT_ALTERNATE_OBJECT_DIRECTORIES'
|
|
86
|
+
|| key === 'GIT_QUARANTINE_PATH') {
|
|
87
|
+
delete env[key];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return env;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Checks whether a changed path belongs to a known test-targetable area.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} file Repository-relative changed file path.
|
|
97
|
+
* @returns {boolean} True when the path can be handled by targeted test selection.
|
|
98
|
+
*/
|
|
99
|
+
function isKnownTargetablePath(file) {
|
|
100
|
+
if (file === '.gitignore') {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (file.startsWith('docs/plans/')) {
|
|
105
|
+
return true;
|
|
37
106
|
}
|
|
107
|
+
|
|
108
|
+
return KNOWN_TARGETABLE_PREFIXES.some((prefix) => file.startsWith(prefix));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Determines whether the resolved test targets require workflow-specific validation.
|
|
113
|
+
*
|
|
114
|
+
* @param {string[]} testTargets Repository-relative test file paths.
|
|
115
|
+
* @returns {boolean} True when workflow tests are part of the target set.
|
|
116
|
+
*/
|
|
117
|
+
function includesWorkflowTarget(testTargets) {
|
|
118
|
+
return testTargets.some((target) => target === 'test/ci-workflow.test.js'
|
|
119
|
+
|| target === 'test/structural/agentic-workflow-sync.test.js'
|
|
120
|
+
|| target.startsWith('test/workflows/'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Deduplicates test targets while preserving the original execution order.
|
|
125
|
+
*
|
|
126
|
+
* @param {string[]} testTargets Repository-relative test file paths.
|
|
127
|
+
* @returns {string[]} Unique test targets in first-seen order.
|
|
128
|
+
*/
|
|
129
|
+
function uniqueTestTargets(testTargets) {
|
|
130
|
+
return [...new Set(testTargets)];
|
|
38
131
|
}
|
|
39
132
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
133
|
+
function isExtraLaneOnlyPath(file) {
|
|
134
|
+
return file.startsWith('test/e2e/') || file.startsWith('test-env/');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Builds the execution plan used by PR, pre-push, and local validation test lanes.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} projectRoot Absolute or relative repository root.
|
|
141
|
+
* @param {typeof defaultExecFileSync} [execFileSync=defaultExecFileSync] Command runner used to inspect Git state.
|
|
142
|
+
* @param {{sinceUpstream?: boolean}} [options={}] Test selection options.
|
|
143
|
+
* @returns {TestExecutionPlan} The computed test execution plan.
|
|
144
|
+
*/
|
|
145
|
+
function buildTestExecutionPlan(projectRoot, execFileSync = defaultExecFileSync, options = {}) {
|
|
146
|
+
const diffOptions = {
|
|
147
|
+
sinceUpstream: options.sinceUpstream !== false,
|
|
148
|
+
};
|
|
149
|
+
const changedFiles = getChangedFiles(execFileSync, diffOptions);
|
|
150
|
+
const affectedTestTargets = getAffectedTestFiles(projectRoot, execFileSync, fs, diffOptions);
|
|
151
|
+
|
|
152
|
+
let runFullSuite = false;
|
|
153
|
+
let runTestEnv = false;
|
|
154
|
+
let runE2E = false;
|
|
155
|
+
let runWorkflowTests = includesWorkflowTarget(affectedTestTargets);
|
|
156
|
+
let hasUnmappedFiles = false;
|
|
157
|
+
const hasUnknownChangedFiles = changedFiles.length === 0 && affectedTestTargets.length === 0;
|
|
158
|
+
|
|
159
|
+
for (const file of changedFiles) {
|
|
160
|
+
if (PACKAGE_LEVEL_PATHS.has(file) || file.startsWith('packages/')) {
|
|
161
|
+
runFullSuite = true;
|
|
162
|
+
runTestEnv = true;
|
|
163
|
+
runE2E = true;
|
|
164
|
+
runWorkflowTests = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (file.startsWith('test-env/')) {
|
|
169
|
+
runTestEnv = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (file.startsWith('test/e2e/')) {
|
|
174
|
+
runE2E = true;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if ((file.startsWith('lib/') || file.startsWith('scripts/')) && (file.endsWith('.js') || file.endsWith('.sh'))) {
|
|
179
|
+
runTestEnv = true;
|
|
180
|
+
if (file === 'scripts/behavioral-judge.sh') {
|
|
181
|
+
runWorkflowTests = true;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
43
185
|
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
186
|
+
if (file.startsWith('.github/workflows/') || file.startsWith('.github/agentic-workflows/')) {
|
|
187
|
+
runWorkflowTests = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isKnownTargetablePath(file)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
hasUnmappedFiles = true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const hasOnlyExtraLaneChanges = changedFiles.length > 0
|
|
198
|
+
&& changedFiles.every(isExtraLaneOnlyPath);
|
|
199
|
+
const hasZeroResolvedTests = changedFiles.length > 0
|
|
200
|
+
&& affectedTestTargets.length === 0
|
|
201
|
+
&& !runFullSuite
|
|
202
|
+
&& !hasOnlyExtraLaneChanges;
|
|
203
|
+
const shouldRunFullSuite = runFullSuite || hasUnmappedFiles || hasUnknownChangedFiles || hasZeroResolvedTests;
|
|
204
|
+
|
|
205
|
+
const reason = hasUnmappedFiles
|
|
206
|
+
? 'unmapped pushed files require full unit coverage'
|
|
207
|
+
: hasUnknownChangedFiles
|
|
208
|
+
? 'changed files could not be resolved safely'
|
|
209
|
+
: hasZeroResolvedTests
|
|
210
|
+
? 'known changes did not resolve runnable tests'
|
|
211
|
+
: runFullSuite
|
|
212
|
+
? 'package-level changes detected'
|
|
213
|
+
: 'known changes mapped to targeted tests';
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
changedFiles,
|
|
217
|
+
hasUnmappedFiles,
|
|
218
|
+
hasUnknownChangedFiles,
|
|
219
|
+
hasZeroResolvedTests,
|
|
220
|
+
mode: shouldRunFullSuite ? 'full' : 'targeted',
|
|
221
|
+
reason,
|
|
222
|
+
runE2E,
|
|
223
|
+
runFullSuite: shouldRunFullSuite,
|
|
224
|
+
runTestEnv,
|
|
225
|
+
runWorkflowTests,
|
|
226
|
+
testTargets: shouldRunFullSuite
|
|
227
|
+
? affectedTestTargets
|
|
228
|
+
: uniqueTestTargets([...affectedTestTargets, ...ALWAYS_RUN_RISK_TEST_TARGETS]),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Classifies the pushed changes into the test plan enforced by the pre-push hook.
|
|
234
|
+
*
|
|
235
|
+
* @param {string} projectRoot Absolute or relative repository root.
|
|
236
|
+
* @param {typeof defaultExecFileSync} [execFileSync=defaultExecFileSync] Command runner used to inspect Git state.
|
|
237
|
+
* @returns {TestExecutionPlan} The pre-push test execution plan.
|
|
238
|
+
*/
|
|
239
|
+
function classifyPushTests(projectRoot, execFileSync = defaultExecFileSync) {
|
|
240
|
+
return buildTestExecutionPlan(projectRoot, execFileSync, { sinceUpstream: true });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Runs a command and returns its exit status, throwing only when process spawning fails.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} command Executable name.
|
|
247
|
+
* @param {string[]} args Command arguments.
|
|
248
|
+
* @param {import('node:child_process').SpawnSyncOptions} [options={}] Spawn options.
|
|
249
|
+
* @param {typeof defaultSpawnSync} [spawnSync=defaultSpawnSync] Process runner.
|
|
250
|
+
* @returns {number} Process exit status, or 1 when no status is reported.
|
|
251
|
+
*/
|
|
252
|
+
function runCommand(command, args, options = {}, spawnSync = defaultSpawnSync) {
|
|
253
|
+
const result = spawnSync(command, args, {
|
|
254
|
+
stdio: 'inherit',
|
|
255
|
+
shell: isWindows,
|
|
256
|
+
...options,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (result.error) {
|
|
260
|
+
throw result.error;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return result.status ?? 1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Executes a computed test plan and any extra targeted validation lanes.
|
|
268
|
+
*
|
|
269
|
+
* @param {TestExecutionPlan} plan Test plan to execute.
|
|
270
|
+
* @param {Object} [deps={}] Runtime dependencies for tests.
|
|
271
|
+
* @returns {number} Exit status for the executed plan.
|
|
272
|
+
*/
|
|
273
|
+
function runTestExecutionPlan(plan, deps = {}) {
|
|
274
|
+
const spawnSync = deps.spawnSync || defaultSpawnSync;
|
|
275
|
+
const pkgManager = deps.pkgManager || detectPackageManager();
|
|
276
|
+
const env = deps.env || stripGitHookEnv(process.env);
|
|
277
|
+
const bunCommand = deps.bunCommand || env.BUN_EXE || process.env.BUN_EXE || 'bun';
|
|
278
|
+
const label = deps.label || 'tests';
|
|
279
|
+
|
|
280
|
+
console.log(`Running ${label} (${pkgManager})...`);
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
if (plan.runFullSuite) {
|
|
284
|
+
console.log(` Mode: full suite (${plan.reason})`);
|
|
285
|
+
const status = runCommand('node', ['scripts/test-full-suite.js'], { env }, spawnSync);
|
|
286
|
+
if (status !== 0) return status;
|
|
287
|
+
} else if (plan.testTargets.length > 0) {
|
|
288
|
+
console.log(` Mode: targeted (${plan.testTargets.length} test file${plan.testTargets.length === 1 ? '' : 's'})`);
|
|
289
|
+
const command = pkgManager === 'bun' ? bunCommand : pkgManager;
|
|
290
|
+
const status = runCommand(command, ['run', 'test', ...plan.testTargets], { env }, spawnSync);
|
|
291
|
+
if (status !== 0) return status;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (plan.runE2E) {
|
|
295
|
+
console.log(' Extra: running affected e2e tests');
|
|
296
|
+
const status = runCommand(bunCommand, ['test', 'test/e2e/'], { env }, spawnSync);
|
|
297
|
+
if (status !== 0) return status;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!plan.runFullSuite && plan.runTestEnv) {
|
|
301
|
+
console.log(' Extra: running affected edge-case tests');
|
|
302
|
+
const status = runCommand(bunCommand, ['test', 'test-env/'], { env }, spawnSync);
|
|
303
|
+
if (status !== 0) return status;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
console.log('Relevant tests passed');
|
|
307
|
+
return 0;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.error('');
|
|
310
|
+
console.error(`Failed to run ${label}: ${error.message}`);
|
|
311
|
+
console.error('');
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Runs the pre-push test plan for the current checkout.
|
|
318
|
+
*
|
|
319
|
+
* @param {string} [projectRoot=process.cwd()] Repository root.
|
|
320
|
+
* @param {Object} [deps={}] Runtime dependencies for tests.
|
|
321
|
+
* @returns {number} Exit status for pre-push tests.
|
|
322
|
+
*/
|
|
323
|
+
function runPrePushTests(projectRoot = process.cwd(), deps = {}) {
|
|
324
|
+
const execFileSync = deps.execFileSync || defaultExecFileSync;
|
|
325
|
+
const plan = classifyPushTests(projectRoot, execFileSync);
|
|
326
|
+
return runTestExecutionPlan(plan, { ...deps, label: 'pre-push tests' });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Runs the local validation test plan for the current checkout.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} [projectRoot=process.cwd()] Repository root.
|
|
333
|
+
* @param {Object} [deps={}] Runtime dependencies for tests.
|
|
334
|
+
* @returns {number} Exit status for local validation tests.
|
|
335
|
+
*/
|
|
336
|
+
function runLocalValidationTests(projectRoot = process.cwd(), deps = {}) {
|
|
337
|
+
const execFileSync = deps.execFileSync || defaultExecFileSync;
|
|
338
|
+
const plan = buildTestExecutionPlan(projectRoot, execFileSync, { sinceUpstream: true });
|
|
339
|
+
return runTestExecutionPlan(plan, { ...deps, label: 'local validation tests' });
|
|
50
340
|
}
|
|
51
341
|
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.error('');
|
|
58
|
-
process.exit(1);
|
|
342
|
+
if (require.main === module) {
|
|
343
|
+
const exitCode = process.argv.includes('--validate')
|
|
344
|
+
? runLocalValidationTests()
|
|
345
|
+
: runPrePushTests();
|
|
346
|
+
process.exit(exitCode);
|
|
59
347
|
}
|
|
60
348
|
|
|
61
|
-
|
|
349
|
+
module.exports = {
|
|
350
|
+
ALWAYS_RUN_RISK_TEST_TARGETS,
|
|
351
|
+
buildTestExecutionPlan,
|
|
352
|
+
classifyPushTests,
|
|
353
|
+
detectPackageManager,
|
|
354
|
+
runLocalValidationTests,
|
|
355
|
+
runPrePushTests,
|
|
356
|
+
runTestExecutionPlan,
|
|
357
|
+
stripGitHookEnv,
|
|
358
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require('node:child_process');
|
|
5
|
+
|
|
6
|
+
const bunCommand = process.env.BUN_EXE || 'bun';
|
|
7
|
+
|
|
8
|
+
function color(code, text) {
|
|
9
|
+
return process.stdout.isTTY ? `\u001b[${code}m${text}\u001b[0m` : text;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function printHeader(title) {
|
|
13
|
+
const line = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
|
|
14
|
+
console.log('');
|
|
15
|
+
console.log(color('0;34', line));
|
|
16
|
+
console.log(color('0;34', `▶ ${title}`));
|
|
17
|
+
console.log(color('0;34', line));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printStatus(kind, message) {
|
|
21
|
+
const palette = {
|
|
22
|
+
error: ['0;31', '✗'],
|
|
23
|
+
success: ['0;32', '✓'],
|
|
24
|
+
warning: ['1;33', '⚠'],
|
|
25
|
+
};
|
|
26
|
+
const [code, prefix] = palette[kind];
|
|
27
|
+
console.log(color(code, `${prefix} ${message}`));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function run(command, args, options = {}) {
|
|
31
|
+
const result = spawnSync(command, args, {
|
|
32
|
+
cwd: process.cwd(),
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
stdio: options.captureOutput ? ['inherit', 'pipe', 'pipe'] : 'inherit',
|
|
35
|
+
shell: false,
|
|
36
|
+
env: options.env || process.env,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (result.error) {
|
|
40
|
+
throw result.error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function combinedOutput(result) {
|
|
47
|
+
return [result.stdout, result.stderr].filter(Boolean).join('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hasBlockingAuditSeverity(output = '') {
|
|
51
|
+
if (typeof output !== 'string' || output.length === 0) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
/\b(?:critical|high)\s+severity\b/i,
|
|
57
|
+
/\bseverity\s*:?\s*(?:critical|high)\b/i,
|
|
58
|
+
].some((pattern) => pattern.test(output));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function main(options = {}) {
|
|
62
|
+
const runCommand = options.runCommand ?? run;
|
|
63
|
+
const activeBunCommand = options.bunCommand ?? bunCommand;
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(color('0;34', '╔═══════════════════════════════════════════╗'));
|
|
66
|
+
console.log(color('0;34', '║ Forge Quality Gate - Running Checks ║'));
|
|
67
|
+
console.log(color('0;34', '╚═══════════════════════════════════════════╝'));
|
|
68
|
+
|
|
69
|
+
printHeader('1/4: Type Check');
|
|
70
|
+
const typecheck = runCommand(activeBunCommand, ['run', 'typecheck'], { captureOutput: true });
|
|
71
|
+
const typecheckOutput = combinedOutput(typecheck);
|
|
72
|
+
if (typecheckOutput) {
|
|
73
|
+
process.stdout.write(typecheckOutput);
|
|
74
|
+
if (!typecheckOutput.endsWith('\n')) {
|
|
75
|
+
process.stdout.write('\n');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if ((typecheck.status ?? 1) !== 0) {
|
|
79
|
+
printStatus('error', 'Type check failed');
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
if (/no typescript in project|skipping type check/i.test(typecheckOutput)) {
|
|
83
|
+
printStatus('warning', 'SKIPPED (no TypeScript in project)');
|
|
84
|
+
} else {
|
|
85
|
+
printStatus('success', 'Type check passed');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
printHeader('2/4: Lint');
|
|
89
|
+
const lint = runCommand(activeBunCommand, ['run', 'lint']);
|
|
90
|
+
if ((lint.status ?? 1) !== 0) {
|
|
91
|
+
printStatus('error', 'Lint failed');
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
printStatus('success', 'Lint passed');
|
|
95
|
+
|
|
96
|
+
printHeader('3/4: Security Audit');
|
|
97
|
+
const audit = runCommand(activeBunCommand, ['audit'], { captureOutput: true });
|
|
98
|
+
const auditOutput = combinedOutput(audit);
|
|
99
|
+
if (auditOutput) {
|
|
100
|
+
process.stdout.write(auditOutput);
|
|
101
|
+
if (!auditOutput.endsWith('\n')) {
|
|
102
|
+
process.stdout.write('\n');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (hasBlockingAuditSeverity(auditOutput)) {
|
|
106
|
+
printStatus('error', 'Security audit found critical/high vulnerabilities');
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
if ((audit.status ?? 1) === 0) {
|
|
110
|
+
printStatus('success', 'Security audit passed');
|
|
111
|
+
} else {
|
|
112
|
+
printStatus('warning', 'Security audit found issues (moderate/low — non-blocking)');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
printHeader('4/4: Tests');
|
|
116
|
+
const tests = runCommand('node', ['scripts/test.js', '--validate']);
|
|
117
|
+
if ((tests.status ?? 1) !== 0) {
|
|
118
|
+
printStatus('error', 'Tests failed');
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
printStatus('success', 'All tests passed');
|
|
122
|
+
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log(color('0;32', '╔═══════════════════════════════════════════╗'));
|
|
125
|
+
console.log(color('0;32', '║ ✓ All Checks Passed Successfully ║'));
|
|
126
|
+
console.log(color('0;32', '╚═══════════════════════════════════════════╝'));
|
|
127
|
+
console.log('');
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (require.main === module) {
|
|
132
|
+
process.exit(main());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
color,
|
|
137
|
+
combinedOutput,
|
|
138
|
+
hasBlockingAuditSeverity,
|
|
139
|
+
main,
|
|
140
|
+
printHeader,
|
|
141
|
+
printStatus,
|
|
142
|
+
run,
|
|
143
|
+
};
|
package/scripts/validate.sh
CHANGED
|
@@ -44,6 +44,23 @@ print_warning() {
|
|
|
44
44
|
echo -e "${YELLOW}⚠ $1${NC}"
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
run_validate_tests() {
|
|
48
|
+
if [[ "${OS:-}" == "Windows_NT" && ( -n "${MSYSTEM:-}" || "${OSTYPE:-}" == msys* || "${OSTYPE:-}" == cygwin* ) ]]; then
|
|
49
|
+
local bun_exe
|
|
50
|
+
bun_exe="$(where.exe bun 2>/dev/null | tr -d '\r' | grep -i 'bun\.exe$' | head -n 1)"
|
|
51
|
+
|
|
52
|
+
if [[ -n "$bun_exe" ]]; then
|
|
53
|
+
cmd.exe /d /c "set \"BUN_EXE=$bun_exe\" && node scripts\\test.js --validate"
|
|
54
|
+
return $?
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
cmd.exe /d /c "node scripts\\test.js --validate"
|
|
58
|
+
return $?
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
node scripts/test.js --validate
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
echo ""
|
|
48
65
|
echo -e "${BLUE}╔═══════════════════════════════════════════╗${NC}"
|
|
49
66
|
echo -e "${BLUE}║ Forge Quality Gate - Running Checks ║${NC}"
|
|
@@ -78,7 +95,7 @@ fi
|
|
|
78
95
|
|
|
79
96
|
# Step 4: Tests
|
|
80
97
|
print_header "4/4: Tests"
|
|
81
|
-
if
|
|
98
|
+
if run_validate_tests; then
|
|
82
99
|
print_success "All tests passed"
|
|
83
100
|
else
|
|
84
101
|
print_error "Tests failed"
|