forge-workflow 0.1.0-beta.6 → 0.1.0-beta.8
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/CHANGELOG.md +36 -0
- package/bin/forge-gh-proxy.js +15 -0
- package/bin/forge-github-credential.js +21 -0
- package/bin/forge.js +16 -1
- package/docs/guides/MIGRATION.md +4 -3
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/VALIDATION.md +14 -0
- package/docs/reference/github-accounts.md +114 -38
- package/lib/bun-workflow-pins.js +129 -3
- package/lib/commands/github.js +234 -49
- package/lib/commands/merge.js +2 -2
- package/lib/commands/push.js +308 -47
- package/lib/commands/release.js +18 -1
- package/lib/commands/shepherd.js +1 -1
- package/lib/commands/test.js +8 -0
- package/lib/commands/validate.js +219 -67
- package/lib/gh-proxy.js +309 -0
- package/lib/github-context.js +160 -25
- package/lib/github-credential.js +47 -0
- package/lib/github-router.js +774 -0
- package/lib/kernel/migrations.js +1 -1
- package/lib/kernel/sqlite-driver.js +1 -1
- package/lib/memory/router.js +1 -1
- package/lib/memory/usage-evidence.js +1 -1
- package/lib/native-gh.js +177 -0
- package/lib/npm-publish-workflow.js +5 -5
- package/lib/pr-monitor/flow-monitor.js +127 -28
- package/lib/pr-monitor/reconcile-executor.js +1 -1
- package/lib/project-memory.js +1 -1
- package/lib/protected-state-authority.js +214 -13
- package/lib/release-readiness.js +57 -41
- package/lib/shell-utils.js +41 -15
- package/lib/test-workflow.js +389 -0
- package/lib/validation-receipt.js +305 -0
- package/lib/workflow-templates/test.yml +654 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +12 -2
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/check-forge-token.js +85 -67
- package/scripts/gen-embedded-assets.mjs +31 -5
- package/scripts/test-full-suite.js +69 -15
- package/scripts/test.js +11 -1
package/lib/commands/push.js
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { randomUUID } = require('node:crypto');
|
|
4
|
+
const { execFileSync, spawn, spawnSync } = require('node:child_process');
|
|
4
5
|
const fs = require('node:fs');
|
|
5
6
|
const path = require('node:path');
|
|
6
7
|
const forgeToken = require('../../scripts/check-forge-token');
|
|
7
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
QUICK_LANE_ENV_VAR,
|
|
10
|
+
QUICK_LANE_VALUE,
|
|
11
|
+
TIMEOUT_EXIT_CODE,
|
|
12
|
+
resolveFullSuiteTimeoutMs,
|
|
13
|
+
runCommand,
|
|
14
|
+
} = require('../../scripts/test');
|
|
15
|
+
const {
|
|
16
|
+
INSTANCE_ENV,
|
|
17
|
+
MANIFEST_ENV,
|
|
18
|
+
TOKEN_ENV,
|
|
19
|
+
createProcessTree,
|
|
20
|
+
} = require('../../scripts/process-tree');
|
|
8
21
|
const { fireAndForget } = require('../pr-monitor/reconcile-executor');
|
|
22
|
+
const validationReceipt = require('../validation-receipt');
|
|
23
|
+
|
|
24
|
+
const { verifyValidationReceipt } = validationReceipt;
|
|
25
|
+
const PUSH_NONCE_ENV_VAR = forgeToken._internal.NONCE_ENV_VAR;
|
|
9
26
|
|
|
10
27
|
const isWindows = process.platform === 'win32';
|
|
11
28
|
|
|
@@ -24,6 +41,19 @@ function detectPackageManager(projectRoot, existsSync = fs.existsSync) {
|
|
|
24
41
|
return 'npm';
|
|
25
42
|
}
|
|
26
43
|
|
|
44
|
+
function isForgeCheckout(projectRoot, readFileSync = fs.readFileSync, existsSync = fs.existsSync) {
|
|
45
|
+
try {
|
|
46
|
+
const manifest = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
47
|
+
return manifest.name === 'forge-workflow'
|
|
48
|
+
&& manifest.bin?.forge === 'bin/forge.js'
|
|
49
|
+
&& manifest.scripts?.['test:full:parallel'] === 'node scripts/test-full-suite.js'
|
|
50
|
+
&& existsSync(path.join(projectRoot, 'scripts', 'test-full-suite.js'));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof SyntaxError || error.code === 'ENOENT' || error.code === 'ENOTDIR') return false;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
/**
|
|
28
58
|
* Get the current git branch name.
|
|
29
59
|
*
|
|
@@ -145,19 +175,26 @@ function maybeTriggerShepherdAfterPush({
|
|
|
145
175
|
* the tests from a `forge push`.
|
|
146
176
|
*
|
|
147
177
|
* @param {boolean} quickMode - Whether this is a --quick push
|
|
148
|
-
* @param {NodeJS.ProcessEnv} [baseEnv
|
|
178
|
+
* @param {NodeJS.ProcessEnv} [baseEnv] - Environment to derive from
|
|
179
|
+
* @param {string} [nonce] - Signed invocation proof locator for the Git child
|
|
149
180
|
* @returns {NodeJS.ProcessEnv} Environment for the git push child process
|
|
150
181
|
*/
|
|
151
|
-
function buildPushEnv(quickMode, baseEnv
|
|
152
|
-
const env = { ...baseEnv };
|
|
182
|
+
function buildPushEnv(quickMode, baseEnv, nonce) {
|
|
183
|
+
const env = { ...(baseEnv || process.env) };
|
|
184
|
+
delete env[PUSH_NONCE_ENV_VAR];
|
|
153
185
|
if (quickMode) {
|
|
154
186
|
env[QUICK_LANE_ENV_VAR] = QUICK_LANE_VALUE;
|
|
155
187
|
} else {
|
|
156
188
|
delete env[QUICK_LANE_ENV_VAR];
|
|
157
189
|
}
|
|
190
|
+
if (nonce) env[PUSH_NONCE_ENV_VAR] = nonce;
|
|
158
191
|
return env;
|
|
159
192
|
}
|
|
160
193
|
|
|
194
|
+
function buildGateEnv(baseEnv = process.env) {
|
|
195
|
+
return buildPushEnv(false, baseEnv);
|
|
196
|
+
}
|
|
197
|
+
|
|
161
198
|
/**
|
|
162
199
|
* Run branch protection check as a subprocess.
|
|
163
200
|
* @param {function} execFn - execFileSync or mock
|
|
@@ -165,9 +202,9 @@ function buildPushEnv(quickMode, baseEnv = process.env) {
|
|
|
165
202
|
* @param {function} log - Logger function
|
|
166
203
|
* @returns {boolean} True if branch protection passed
|
|
167
204
|
*/
|
|
168
|
-
function runBranchProtection(execFn, projectRoot, log) {
|
|
205
|
+
function runBranchProtection(execFn, projectRoot, log, env) {
|
|
169
206
|
try {
|
|
170
|
-
execFn('node', [path.join(projectRoot, 'scripts', 'branch-protection.js')]);
|
|
207
|
+
execFn('node', [path.join(projectRoot, 'scripts', 'branch-protection.js')], { env });
|
|
171
208
|
return true;
|
|
172
209
|
} catch (_err) { /* intentional: fallback to failure */ // NOSONAR S2486
|
|
173
210
|
log('Branch protection check failed — push aborted.');
|
|
@@ -182,11 +219,12 @@ function runBranchProtection(execFn, projectRoot, log) {
|
|
|
182
219
|
* @param {string} projectRoot - Absolute path to project root
|
|
183
220
|
* @returns {boolean} True if lint passed
|
|
184
221
|
*/
|
|
185
|
-
function runLint(spawnFn, pkgManager, projectRoot) {
|
|
222
|
+
function runLint(spawnFn, pkgManager, projectRoot, env) {
|
|
186
223
|
const lintResult = spawnFn(pkgManager, ['run', 'lint'], {
|
|
187
224
|
stdio: 'inherit',
|
|
188
225
|
shell: isWindows,
|
|
189
226
|
cwd: projectRoot,
|
|
227
|
+
env,
|
|
190
228
|
});
|
|
191
229
|
return lintResult.status === 0;
|
|
192
230
|
}
|
|
@@ -279,18 +317,104 @@ function reportTestRunTermination(testResult, timeoutMs, log) {
|
|
|
279
317
|
log('Test run ended without an exit status — push aborted.');
|
|
280
318
|
}
|
|
281
319
|
|
|
320
|
+
async function runSupervisedTestCommand({
|
|
321
|
+
testCommand,
|
|
322
|
+
testArgs,
|
|
323
|
+
projectRoot,
|
|
324
|
+
env,
|
|
325
|
+
timeoutMs,
|
|
326
|
+
log,
|
|
327
|
+
spawnAsyncFn,
|
|
328
|
+
createProcessTreeFn,
|
|
329
|
+
}) {
|
|
330
|
+
const isolatedEnv = { ...env };
|
|
331
|
+
delete isolatedEnv[MANIFEST_ENV];
|
|
332
|
+
delete isolatedEnv[TOKEN_ENV];
|
|
333
|
+
delete isolatedEnv[INSTANCE_ENV];
|
|
334
|
+
// A unique manifest prevents timeout cleanup from touching an outer
|
|
335
|
+
// validation run while allInstances reaches this runner's nested shards.
|
|
336
|
+
const processTree = createProcessTreeFn({
|
|
337
|
+
env: isolatedEnv,
|
|
338
|
+
token: randomUUID(),
|
|
339
|
+
allInstances: true,
|
|
340
|
+
reconcile: false,
|
|
341
|
+
});
|
|
342
|
+
let supervisedEnv = null;
|
|
343
|
+
try {
|
|
344
|
+
supervisedEnv = processTree.envFor(isolatedEnv);
|
|
345
|
+
} catch { /* handled by the fail-closed check below */ }
|
|
346
|
+
const supervisionMarker = supervisedEnv?.[MANIFEST_ENV];
|
|
347
|
+
if (typeof supervisionMarker !== 'string' || supervisionMarker.length === 0) {
|
|
348
|
+
log('Test supervision unavailable — push aborted.');
|
|
349
|
+
processTree.cleanup('SIGKILL');
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
let cancellationSignal = null;
|
|
353
|
+
const removeSignalHandlers = processTree.installSignalHandlers((signal) => {
|
|
354
|
+
cancellationSignal = signal;
|
|
355
|
+
processTree.cleanup('SIGKILL');
|
|
356
|
+
});
|
|
357
|
+
let terminationSignal = null;
|
|
358
|
+
const observingSpawn = (...spawnArgs) => {
|
|
359
|
+
const child = spawnAsyncFn(...spawnArgs);
|
|
360
|
+
child.once?.('close', (_status, signal) => {
|
|
361
|
+
terminationSignal = signal;
|
|
362
|
+
});
|
|
363
|
+
return child;
|
|
364
|
+
};
|
|
365
|
+
try {
|
|
366
|
+
if (cancellationSignal) return false;
|
|
367
|
+
const status = await runCommand(testCommand, testArgs, {
|
|
368
|
+
cwd: projectRoot,
|
|
369
|
+
detached: !isWindows,
|
|
370
|
+
env: supervisedEnv,
|
|
371
|
+
killSignal: TEST_RUN_KILL_SIGNAL,
|
|
372
|
+
processTree,
|
|
373
|
+
timeout: timeoutMs,
|
|
374
|
+
windowsHide: true,
|
|
375
|
+
}, observingSpawn);
|
|
376
|
+
if (cancellationSignal) {
|
|
377
|
+
reportTestRunTermination({ status: null, signal: cancellationSignal }, timeoutMs, log);
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
if (status === TIMEOUT_EXIT_CODE) {
|
|
381
|
+
reportTestRunTermination({
|
|
382
|
+
status: null,
|
|
383
|
+
signal: TEST_RUN_KILL_SIGNAL,
|
|
384
|
+
error: Object.assign(new Error('test run timed out'), { code: 'ETIMEDOUT' }),
|
|
385
|
+
}, timeoutMs, log);
|
|
386
|
+
}
|
|
387
|
+
if (terminationSignal) {
|
|
388
|
+
reportTestRunTermination({ status: null, signal: terminationSignal }, timeoutMs, log);
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
return status === 0;
|
|
392
|
+
} catch (error) {
|
|
393
|
+
reportTestRunTermination({ status: null, signal: null, error }, timeoutMs, log);
|
|
394
|
+
return false;
|
|
395
|
+
} finally {
|
|
396
|
+
removeSignalHandlers();
|
|
397
|
+
processTree.cleanup(cancellationSignal ? 'SIGKILL' : 'SIGTERM');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
282
401
|
/**
|
|
283
402
|
* Run tests unless in quick mode, logging warnings for first push.
|
|
284
|
-
* @param {
|
|
285
|
-
* @
|
|
286
|
-
* @param {string} pkgManager - Package manager command
|
|
287
|
-
* @param {string} projectRoot - Absolute path to project root
|
|
288
|
-
* @param {boolean} quickMode - Whether to skip tests
|
|
289
|
-
* @param {function} log - Logger function
|
|
290
|
-
* @param {NodeJS.ProcessEnv} [env] - Environment used to resolve the test budget
|
|
291
|
-
* @returns {boolean|undefined} True if tests passed, undefined if skipped
|
|
403
|
+
* @param {object} options - Test execution dependencies and state
|
|
404
|
+
* @returns {Promise<boolean|undefined>} True if tests passed, undefined if skipped
|
|
292
405
|
*/
|
|
293
|
-
function runTests(
|
|
406
|
+
async function runTests({
|
|
407
|
+
spawnAsyncFn,
|
|
408
|
+
execFn,
|
|
409
|
+
readFileFn,
|
|
410
|
+
existsFn,
|
|
411
|
+
pkgManager,
|
|
412
|
+
projectRoot,
|
|
413
|
+
quickMode,
|
|
414
|
+
log,
|
|
415
|
+
env = process.env,
|
|
416
|
+
createProcessTreeFn = createProcessTree,
|
|
417
|
+
}) {
|
|
294
418
|
if (quickMode) {
|
|
295
419
|
log('Tests skipped (--quick) — CI will run full suite on GitHub');
|
|
296
420
|
const branch = getCurrentBranch(execFn);
|
|
@@ -300,7 +424,10 @@ function runTests(spawnFn, execFn, pkgManager, projectRoot, quickMode, log, env
|
|
|
300
424
|
return undefined;
|
|
301
425
|
}
|
|
302
426
|
|
|
303
|
-
|
|
427
|
+
const useForgeRunner = isForgeCheckout(projectRoot, readFileFn, existsFn);
|
|
428
|
+
const testCommand = useForgeRunner ? 'node' : pkgManager;
|
|
429
|
+
const testArgs = useForgeRunner ? ['scripts/test-full-suite.js'] : ['run', 'test'];
|
|
430
|
+
|
|
304
431
|
// Budget it from the shared full-suite budget in scripts/test.js — which is
|
|
305
432
|
// sized as ~2x the measured healthy full-suite runtime and overridable with
|
|
306
433
|
// FORGE_TEST_TIMEOUT_MS — instead of an arbitrary local cap: the old fixed
|
|
@@ -308,18 +435,108 @@ function runTests(spawnFn, execFn, pkgManager, projectRoot, quickMode, log, env
|
|
|
308
435
|
// reintroduce a local constant here; change the shared budget with a fresh
|
|
309
436
|
// measurement instead.
|
|
310
437
|
const timeoutMs = resolveFullSuiteTimeoutMs(env);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
438
|
+
return runSupervisedTestCommand({
|
|
439
|
+
testCommand,
|
|
440
|
+
testArgs,
|
|
441
|
+
projectRoot,
|
|
442
|
+
env,
|
|
443
|
+
timeoutMs,
|
|
444
|
+
log,
|
|
445
|
+
spawnAsyncFn,
|
|
446
|
+
createProcessTreeFn,
|
|
317
447
|
});
|
|
318
|
-
|
|
319
|
-
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Consume Forge-owned push arguments while preserving Git-owned arguments.
|
|
452
|
+
* Only the first `--` is a Forge delimiter; everything after it belongs to Git.
|
|
453
|
+
*
|
|
454
|
+
* @param {string[]} args - Raw command arguments
|
|
455
|
+
* @param {object} flags - Parsed Forge flags
|
|
456
|
+
* @returns {{ quickMode: boolean, gitArgs: string[] }} Parsed push arguments
|
|
457
|
+
*/
|
|
458
|
+
function parsePushArguments(args, flags) {
|
|
459
|
+
const delimiterIndex = args.indexOf('--');
|
|
460
|
+
const forgeArgs = delimiterIndex === -1 ? args : args.slice(0, delimiterIndex);
|
|
461
|
+
const gitArgsAfterDelimiter = delimiterIndex === -1 ? [] : args.slice(delimiterIndex + 1);
|
|
462
|
+
const quickMode = delimiterIndex === -1
|
|
463
|
+
? Boolean(flags && (flags['--quick'] || flags.quick))
|
|
464
|
+
: forgeArgs.some(arg => arg === '--quick' || arg === '-q');
|
|
465
|
+
return {
|
|
466
|
+
quickMode,
|
|
467
|
+
gitArgs: [
|
|
468
|
+
...forgeArgs.filter(arg => arg !== '--quick' && arg !== '-q'),
|
|
469
|
+
...gitArgsAfterDelimiter,
|
|
470
|
+
],
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Bind full-mode authorization to either the reused receipt or this fresh run.
|
|
476
|
+
* Quick mode has no test evidence and must carry no receipt identity.
|
|
477
|
+
*/
|
|
478
|
+
function receiptIdentityForPush(quickMode, validationReused, receipt) {
|
|
479
|
+
if (quickMode) return null;
|
|
480
|
+
if (validationReused) return receipt.identity;
|
|
481
|
+
return 'fresh-tests';
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Write the short-lived pre-push authorization after all required gates pass.
|
|
486
|
+
* Failure is non-fatal because Lefthook then executes its gates normally.
|
|
487
|
+
*/
|
|
488
|
+
function writePushAuthorization({
|
|
489
|
+
tokenWrite,
|
|
490
|
+
worktreeRoot,
|
|
491
|
+
proofSnapshot,
|
|
492
|
+
quickMode,
|
|
493
|
+
completedGates,
|
|
494
|
+
validationReused,
|
|
495
|
+
receipt,
|
|
496
|
+
gateEnv,
|
|
497
|
+
log,
|
|
498
|
+
}) {
|
|
499
|
+
try {
|
|
500
|
+
return tokenWrite(worktreeRoot, {
|
|
501
|
+
snapshot: proofSnapshot,
|
|
502
|
+
mode: quickMode ? 'quick' : 'full',
|
|
503
|
+
gates: completedGates,
|
|
504
|
+
receiptIdentity: receiptIdentityForPush(quickMode, validationReused, receipt),
|
|
505
|
+
env: gateEnv,
|
|
506
|
+
});
|
|
507
|
+
} catch (_err) { /* intentional: non-fatal, lefthook will re-run hooks */ // NOSONAR S2486
|
|
508
|
+
log('Warning: Could not write forge push token — lefthook hooks will run again');
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Execute Git push and revoke this invocation's proof regardless of outcome.
|
|
515
|
+
*/
|
|
516
|
+
function executeGitPush({
|
|
517
|
+
execFn,
|
|
518
|
+
gitArgs,
|
|
519
|
+
quickMode,
|
|
520
|
+
baseEnv,
|
|
521
|
+
authorization,
|
|
522
|
+
tokenConsume,
|
|
523
|
+
worktreeRoot,
|
|
524
|
+
log,
|
|
525
|
+
}) {
|
|
526
|
+
try {
|
|
527
|
+
execFn('git', ['push', ...gitArgs], {
|
|
528
|
+
stdio: 'inherit',
|
|
529
|
+
env: buildPushEnv(quickMode, baseEnv, authorization?.nonce),
|
|
530
|
+
});
|
|
531
|
+
return true;
|
|
532
|
+
} catch (_pushErr) { // NOSONAR S2486
|
|
533
|
+
log('git push failed.');
|
|
320
534
|
return false;
|
|
535
|
+
} finally {
|
|
536
|
+
if (authorization?.nonce) {
|
|
537
|
+
try { tokenConsume(worktreeRoot, { nonce: authorization.nonce }); } catch { /* best effort revocation */ }
|
|
538
|
+
}
|
|
321
539
|
}
|
|
322
|
-
return testResult.status === 0;
|
|
323
540
|
}
|
|
324
541
|
|
|
325
542
|
module.exports = {
|
|
@@ -343,56 +560,99 @@ module.exports = {
|
|
|
343
560
|
handler: async (args, flags, projectRoot, deps) => {
|
|
344
561
|
const execFn = deps?.execFileSync || execFileSync;
|
|
345
562
|
const spawnFn = deps?.spawnSync || spawnSync;
|
|
563
|
+
const spawnAsyncFn = deps?.spawn || spawn;
|
|
346
564
|
const existsFn = deps?.existsSync || fs.existsSync;
|
|
565
|
+
const readFileFn = deps?.readFileSync || fs.readFileSync;
|
|
347
566
|
const log = deps?.log || console.log;
|
|
348
567
|
const tokenWrite = deps?.writeForgeToken || forgeToken.write;
|
|
568
|
+
const tokenConsume = deps?.consumeForgeToken || forgeToken.consume;
|
|
569
|
+
const beginPushProof = deps?.beginPushProof || validationReceipt._pushProof.begin;
|
|
570
|
+
const verifyReceipt = deps?.verifyValidationReceipt || verifyValidationReceipt;
|
|
349
571
|
|
|
350
|
-
const quickMode
|
|
572
|
+
const { quickMode, gitArgs } = parsePushArguments(args, flags);
|
|
351
573
|
const pkgManager = detectPackageManager(projectRoot, existsFn);
|
|
574
|
+
const worktreeRoot = resolveGitToplevel(execFn, projectRoot);
|
|
575
|
+
const gateEnv = buildGateEnv(deps?.env || process.env);
|
|
576
|
+
let proofSnapshot = null;
|
|
577
|
+
try {
|
|
578
|
+
proofSnapshot = beginPushProof(worktreeRoot, { env: gateEnv });
|
|
579
|
+
} catch { /* invalid snapshot falls back to ordinary hooks */ }
|
|
580
|
+
const completedGates = [];
|
|
352
581
|
|
|
353
582
|
// Step 1: Branch protection
|
|
354
|
-
const branchOk = runBranchProtection(execFn, projectRoot, log);
|
|
583
|
+
const branchOk = runBranchProtection(execFn, projectRoot, log, gateEnv);
|
|
355
584
|
if (!branchOk) {
|
|
356
585
|
return { success: false, quickMode, lintPassed: false, pushed: false };
|
|
357
586
|
}
|
|
587
|
+
completedGates.push('branch-protection');
|
|
358
588
|
|
|
359
589
|
// Auto-file rail (non-blocking): ensure the branch being pushed has a backing
|
|
360
590
|
// Kernel issue so started work never goes unfiled. Never aborts the push.
|
|
361
591
|
await autoFileBackingIssueForPush(projectRoot, execFn, deps || {});
|
|
362
592
|
|
|
363
593
|
// Step 2: Lint
|
|
364
|
-
const lintPassed = runLint(spawnFn, pkgManager, projectRoot);
|
|
594
|
+
const lintPassed = runLint(spawnFn, pkgManager, projectRoot, gateEnv);
|
|
365
595
|
if (!lintPassed) {
|
|
366
596
|
log('Lint failed — push aborted.');
|
|
367
597
|
return { success: false, quickMode, lintPassed: false, pushed: false };
|
|
368
598
|
}
|
|
599
|
+
completedGates.push('lint');
|
|
369
600
|
|
|
370
601
|
// Step 3: Tests (skip in quick mode)
|
|
371
|
-
const
|
|
602
|
+
const receipt = quickMode
|
|
603
|
+
? { valid: false }
|
|
604
|
+
: verifyReceipt(worktreeRoot);
|
|
605
|
+
const validationReused = receipt.valid === true;
|
|
606
|
+
if (validationReused) log(`Tests reused from full validation at ${receipt.head || 'current HEAD'}`);
|
|
607
|
+
const testsPassed = validationReused
|
|
608
|
+
? true
|
|
609
|
+
: await runTests({
|
|
610
|
+
spawnAsyncFn,
|
|
611
|
+
execFn,
|
|
612
|
+
readFileFn,
|
|
613
|
+
existsFn,
|
|
614
|
+
pkgManager,
|
|
615
|
+
projectRoot,
|
|
616
|
+
quickMode,
|
|
617
|
+
log,
|
|
618
|
+
env: gateEnv,
|
|
619
|
+
createProcessTreeFn: deps?.createProcessTree || createProcessTree,
|
|
620
|
+
});
|
|
372
621
|
if (!quickMode && !testsPassed) {
|
|
373
|
-
return { success: false, quickMode, lintPassed: true, testsPassed: false, pushed: false };
|
|
622
|
+
return { success: false, quickMode, lintPassed: true, testsPassed: false, validationReused, pushed: false };
|
|
374
623
|
}
|
|
624
|
+
if (!quickMode) completedGates.push('tests');
|
|
375
625
|
|
|
376
|
-
// Step 4: Write
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
626
|
+
// Step 4: Write invocation proof under worktree-private Git metadata.
|
|
627
|
+
const authorization = writePushAuthorization({
|
|
628
|
+
tokenWrite,
|
|
629
|
+
worktreeRoot,
|
|
630
|
+
proofSnapshot,
|
|
631
|
+
quickMode,
|
|
632
|
+
completedGates,
|
|
633
|
+
validationReused,
|
|
634
|
+
receipt,
|
|
635
|
+
gateEnv,
|
|
636
|
+
log,
|
|
637
|
+
});
|
|
384
638
|
|
|
385
639
|
// Step 5: git push with passthrough args
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
640
|
+
const pushed = executeGitPush({
|
|
641
|
+
execFn,
|
|
642
|
+
gitArgs,
|
|
643
|
+
quickMode,
|
|
644
|
+
baseEnv: deps?.env || process.env,
|
|
645
|
+
authorization,
|
|
646
|
+
tokenConsume,
|
|
647
|
+
worktreeRoot,
|
|
648
|
+
log,
|
|
649
|
+
});
|
|
650
|
+
if (!pushed) {
|
|
391
651
|
return {
|
|
392
652
|
success: false,
|
|
393
653
|
quickMode,
|
|
394
654
|
lintPassed: true,
|
|
395
|
-
...(quickMode ? {} : { testsPassed: true }),
|
|
655
|
+
...(quickMode ? {} : { testsPassed: true, validationReused }),
|
|
396
656
|
pushed: false,
|
|
397
657
|
};
|
|
398
658
|
}
|
|
@@ -407,7 +667,7 @@ module.exports = {
|
|
|
407
667
|
success: true,
|
|
408
668
|
quickMode,
|
|
409
669
|
lintPassed: true,
|
|
410
|
-
...(quickMode ? {} : { testsPassed: true }),
|
|
670
|
+
...(quickMode ? {} : { testsPassed: true, validationReused }),
|
|
411
671
|
pushed: true,
|
|
412
672
|
};
|
|
413
673
|
},
|
|
@@ -415,6 +675,7 @@ module.exports = {
|
|
|
415
675
|
// Exposed for unit tests; not part of the CLI surface.
|
|
416
676
|
_internal: {
|
|
417
677
|
autoFileBackingIssueForPush,
|
|
678
|
+
buildGateEnv,
|
|
418
679
|
buildPushEnv,
|
|
419
680
|
maybeTriggerShepherdAfterPush,
|
|
420
681
|
},
|
package/lib/commands/release.js
CHANGED
|
@@ -9,13 +9,14 @@ const {
|
|
|
9
9
|
const { runIssueOperation: defaultRunIssueOperation } = require('../forge-issues');
|
|
10
10
|
const { normalizeArgs, normalizeIssueResult, withResolvedIssueBackend } = require('./_issue');
|
|
11
11
|
const { generateNpmPublishWorkflow } = require('../npm-publish-workflow');
|
|
12
|
+
const { generateTestWorkflow } = require('../test-workflow');
|
|
12
13
|
const { updateBunWorkflowPins } = require('../bun-workflow-pins');
|
|
13
14
|
|
|
14
15
|
// `forge release <id>` releases a claimed issue; `forge release check` runs the
|
|
15
16
|
// release-readiness gate. The two share the top-level verb, so this command
|
|
16
17
|
// dispatches `check` to the gate and routes everything else through the shared
|
|
17
18
|
// issue dispatch (resolve backend → runIssueOperation('release') → normalize).
|
|
18
|
-
const usage = 'Usage: forge release <id> | forge release check --target 0.1.0 [--json] | forge release regen-audit | forge release generate-npm-workflow --expect-head <full-sha> | forge release update-bun-pins --expect-head <full-sha>';
|
|
19
|
+
const usage = 'Usage: forge release <id> | forge release check --target 0.1.0 [--json] | forge release regen-audit | forge release generate-npm-workflow --expect-head <full-sha> | forge release generate-test-workflow --expect-head <full-sha> | forge release update-bun-pins --expect-head <full-sha>';
|
|
19
20
|
|
|
20
21
|
async function runReleaseIssue(args, projectRoot, opts = {}) {
|
|
21
22
|
const resolved = withResolvedIssueBackend(projectRoot, opts);
|
|
@@ -59,6 +60,22 @@ function parseReleaseArgs(args = []) {
|
|
|
59
60
|
|
|
60
61
|
async function handler(args, _flags, projectRoot, opts = {}) {
|
|
61
62
|
const parsed = parseReleaseArgs(args);
|
|
63
|
+
if (parsed.subcommand === 'generate-test-workflow') {
|
|
64
|
+
const generator = opts.generateTestWorkflow || generateTestWorkflow;
|
|
65
|
+
const generated = await generator(projectRoot, {
|
|
66
|
+
env: opts.env,
|
|
67
|
+
kernelDeps: opts.kernelDeps,
|
|
68
|
+
expectedHead: parsed.expectedHead,
|
|
69
|
+
resolveHead: opts.resolveHead,
|
|
70
|
+
});
|
|
71
|
+
return generated.success
|
|
72
|
+
? {
|
|
73
|
+
success: true,
|
|
74
|
+
generated,
|
|
75
|
+
output: `Generated ${generated.path} (${generated.contentHash}).\n`,
|
|
76
|
+
}
|
|
77
|
+
: generated;
|
|
78
|
+
}
|
|
62
79
|
if (parsed.subcommand === 'update-bun-pins') {
|
|
63
80
|
const updater = opts.updateBunWorkflowPins || updateBunWorkflowPins;
|
|
64
81
|
const updated = await updater(projectRoot, {
|
package/lib/commands/shepherd.js
CHANGED
|
@@ -313,7 +313,7 @@ async function buildMonitorContext(pr, projectRoot, deps) {
|
|
|
313
313
|
const buildKernelDeps = deps.buildKernelDeps
|
|
314
314
|
|| require('../kernel/cli-broker-factory').buildMigratedKernelIssueDeps;
|
|
315
315
|
const createMonitorStore = deps.createMonitorStore
|
|
316
|
-
|| require('
|
|
316
|
+
|| require('@forge/memory').createMonitorStore;
|
|
317
317
|
let kernel;
|
|
318
318
|
try {
|
|
319
319
|
kernel = await buildKernelDeps({ projectRoot: projectRoot || process.cwd(), gitCommonDir });
|
package/lib/commands/test.js
CHANGED
|
@@ -28,6 +28,8 @@ const LOCKFILE_MAP = [
|
|
|
28
28
|
|
|
29
29
|
const DEFAULT_TIMEOUT = 120000;
|
|
30
30
|
const DIRECT_TEST_CANDIDATES = Object.freeze({
|
|
31
|
+
'lib/test-workflow.js': ['test/test-workflow.test.js'],
|
|
32
|
+
'lib/workflow-templates/test.yml': ['test/test-workflow.test.js'],
|
|
31
33
|
'bin/forge.js': [
|
|
32
34
|
'test/cli-flags.test.js',
|
|
33
35
|
'test/forge-cli-registry.test.js',
|
|
@@ -86,7 +88,13 @@ const DIRECT_TEST_CANDIDATES = Object.freeze({
|
|
|
86
88
|
'test/github-launcher.test.js',
|
|
87
89
|
'test/integration/github-account-context.test.js',
|
|
88
90
|
],
|
|
91
|
+
'bin/forge-gh-proxy.js': ['test/bin/forge-gh-proxy.test.js', 'test/github-launcher.test.js'],
|
|
92
|
+
'bin/forge-github-credential.js': ['test/bin/forge-github-credential.test.js', 'test/github-launcher.test.js'],
|
|
89
93
|
'lib/github-context.js': ['test/github-context.test.js', 'test/integration/github-account-context.test.js'],
|
|
94
|
+
'lib/github-credential.js': ['test/github-credential.test.js', 'test/github-launcher.test.js'],
|
|
95
|
+
'lib/github-router.js': ['test/github-router.test.js', 'test/commands/github.test.js'],
|
|
96
|
+
'lib/gh-proxy.js': ['test/gh-proxy.test.js', 'test/github-launcher.test.js'],
|
|
97
|
+
'lib/native-gh.js': ['test/native-gh.test.js', 'test/integration/github-account-context.test.js'],
|
|
90
98
|
'lib/commands/_registry.js': [
|
|
91
99
|
'test/commands/_registry.test.js',
|
|
92
100
|
'test/commands/_registry-github-context.test.js',
|