@deftai/directive-core 0.80.0 → 0.82.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/dist/content-contracts/skills/helpers.d.ts +6 -0
- package/dist/content-contracts/skills/helpers.js +47 -3
- package/dist/content-contracts/skills/skill-external-fetch-gate.d.ts +18 -0
- package/dist/content-contracts/skills/skill-external-fetch-gate.js +81 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +139 -0
- package/dist/doctor/main.js +2 -1
- package/dist/eval/crud-telemetry.d.ts +2 -0
- package/dist/eval/crud-telemetry.js +30 -0
- package/dist/intake/github-body-cli.js +2 -0
- package/dist/intake/github-body.d.ts +11 -0
- package/dist/intake/github-body.js +95 -24
- package/dist/integration-e2e/helpers.js +1 -1
- package/dist/issue-sync/sync-from-xbrief.js +3 -0
- package/dist/orphan-active/evaluate.d.ts +25 -0
- package/dist/orphan-active/evaluate.js +275 -0
- package/dist/orphan-active/index.d.ts +3 -0
- package/dist/orphan-active/index.js +3 -0
- package/dist/orphan-active/refs.d.ts +16 -0
- package/dist/orphan-active/refs.js +105 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +19 -5
- package/dist/policy/product-signal.d.ts +45 -0
- package/dist/policy/product-signal.js +210 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.js +18 -0
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +44 -73
- package/dist/pr-wait-mergeable/result.d.ts +2 -1
- package/dist/pr-wait-mergeable/result.js +4 -0
- package/dist/pr-wait-mergeable/semantic-green.d.ts +27 -0
- package/dist/pr-wait-mergeable/semantic-green.js +202 -0
- package/dist/pr-wait-mergeable/types.d.ts +1 -0
- package/dist/pr-watch/constants.d.ts +4 -4
- package/dist/pr-watch/constants.js +4 -4
- package/dist/pr-watch/watch.js +5 -3
- package/dist/product-signal/actor-name.d.ts +18 -0
- package/dist/product-signal/actor-name.js +94 -0
- package/dist/product-signal/consent.d.ts +30 -0
- package/dist/product-signal/consent.js +116 -0
- package/dist/product-signal/gates.d.ts +17 -0
- package/dist/product-signal/gates.js +66 -0
- package/dist/product-signal/github-private-sink-adapter.d.ts +28 -0
- package/dist/product-signal/github-private-sink-adapter.js +274 -0
- package/dist/product-signal/headless.d.ts +8 -0
- package/dist/product-signal/headless.js +32 -0
- package/dist/product-signal/install-context.d.ts +13 -0
- package/dist/product-signal/install-context.js +41 -0
- package/dist/product-signal/local-signal-summary.d.ts +7 -0
- package/dist/product-signal/local-signal-summary.js +173 -0
- package/dist/product-signal/payload.d.ts +90 -0
- package/dist/product-signal/payload.js +124 -0
- package/dist/product-signal/sink-bootstrap.d.ts +29 -0
- package/dist/product-signal/sink-bootstrap.js +108 -0
- package/dist/product-signal/submit-adapter.d.ts +14 -0
- package/dist/product-signal/submit-adapter.js +2 -0
- package/dist/product-signal/submit.d.ts +54 -0
- package/dist/product-signal/submit.js +290 -0
- package/dist/release/native-steps.js +11 -4
- package/dist/render/framework-commands.js +6 -0
- package/dist/scm/gh-rest.d.ts +3 -1
- package/dist/scm/gh-rest.js +22 -0
- package/dist/scope/brief-io.d.ts +26 -0
- package/dist/scope/brief-io.js +77 -0
- package/dist/scope/decompose.js +2 -2
- package/dist/scope/decomposed-refs.js +3 -3
- package/dist/scope/demote.js +2 -2
- package/dist/scope/project-definition-sync.d.ts +2 -2
- package/dist/scope/project-definition-sync.js +15 -3
- package/dist/scope/registry-artifact-sync.d.ts +6 -1
- package/dist/scope/registry-artifact-sync.js +31 -13
- package/dist/scope/transition.js +23 -15
- package/dist/scope/undo.js +2 -2
- package/dist/scope/vbrief-json.d.ts +1 -2
- package/dist/scope/vbrief-json.js +1 -4
- package/dist/staleness-tickler/state.js +19 -2
- package/dist/triage/queue/cache.js +5 -0
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/skill-external-fetch-gate.d.ts +21 -0
- package/dist/verify-source/skill-external-fetch-gate.js +71 -0
- package/package.json +7 -3
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
3
|
+
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
4
|
+
import { policyColonInvocation } from "./policy-invocation.js";
|
|
5
|
+
import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
|
|
6
|
+
/** Canonical registered policy field name (#2693). */
|
|
7
|
+
export const FIELD_PRODUCT_SIGNAL = "plan.policy.productSignal";
|
|
8
|
+
/** Short alias for `policy:show --field=productSignal`. */
|
|
9
|
+
export const FIELD_PRODUCT_SIGNAL_CLI_ALIAS = "productSignal";
|
|
10
|
+
export const DEFAULT_PRODUCT_SIGNAL_ENABLED = false;
|
|
11
|
+
/** Baked-in default private sink (#2693 D6). */
|
|
12
|
+
export const DEFAULT_PRODUCT_SIGNAL_SINK_REPO = "deftai/product-signal";
|
|
13
|
+
export const PRODUCT_SIGNAL_CAPABILITY_COST_DISCLOSURE = "\u26a0 Capability-cost disclosure -- enabling product signal opts into " +
|
|
14
|
+
"consented qualitative check-ins that may submit minimized local summaries " +
|
|
15
|
+
"to a private GitHub sink when you also grant install-level consent.\n" +
|
|
16
|
+
" \u2022 Default OFF; no ambient consent nag while disabled.\n" +
|
|
17
|
+
" \u2022 Outbound requires enable + recorded consent (`product-signal:consent`).\n" +
|
|
18
|
+
" \u2022 Payloads include install context and windowed value/health/helped summaries " +
|
|
19
|
+
"(not raw ledgers or chat).\n" +
|
|
20
|
+
" \u2022 Inspect: `" +
|
|
21
|
+
policyColonInvocation("show", " --field=productSignal") +
|
|
22
|
+
"` and `task product-signal:status`.\n" +
|
|
23
|
+
" \u2022 Reversible: set `enabled: false` or revoke consent.\n" +
|
|
24
|
+
" \u2022 Changes are recorded to meta/policy-changes.log for auditability.";
|
|
25
|
+
function defaultResolved(source, error = null) {
|
|
26
|
+
return {
|
|
27
|
+
enabled: DEFAULT_PRODUCT_SIGNAL_ENABLED,
|
|
28
|
+
sinkRepo: DEFAULT_PRODUCT_SIGNAL_SINK_REPO,
|
|
29
|
+
source,
|
|
30
|
+
error,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Validate a `plan.policy.productSignal` payload. */
|
|
34
|
+
export function validateProductSignal(value) {
|
|
35
|
+
if (value === null || value === undefined) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
39
|
+
return [`${FIELD_PRODUCT_SIGNAL} must be an object; got ${typeof value}`];
|
|
40
|
+
}
|
|
41
|
+
const rec = value;
|
|
42
|
+
const errors = [];
|
|
43
|
+
if ("enabled" in rec && typeof rec.enabled !== "boolean") {
|
|
44
|
+
errors.push(`${FIELD_PRODUCT_SIGNAL}.enabled must be a boolean`);
|
|
45
|
+
}
|
|
46
|
+
if ("sinkRepo" in rec && typeof rec.sinkRepo !== "string") {
|
|
47
|
+
errors.push(`${FIELD_PRODUCT_SIGNAL}.sinkRepo must be a string`);
|
|
48
|
+
}
|
|
49
|
+
return errors;
|
|
50
|
+
}
|
|
51
|
+
function resolveFromPolicyBlock(raw) {
|
|
52
|
+
const errors = validateProductSignal(raw);
|
|
53
|
+
if (errors.length > 0) {
|
|
54
|
+
return defaultResolved("default-on-error", errors[0] ?? "invalid productSignal block");
|
|
55
|
+
}
|
|
56
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
57
|
+
return defaultResolved("default");
|
|
58
|
+
}
|
|
59
|
+
const block = raw;
|
|
60
|
+
const enabled = typeof block.enabled === "boolean" ? block.enabled : DEFAULT_PRODUCT_SIGNAL_ENABLED;
|
|
61
|
+
const sinkRepo = typeof block.sinkRepo === "string" && block.sinkRepo.trim().length > 0
|
|
62
|
+
? block.sinkRepo.trim()
|
|
63
|
+
: DEFAULT_PRODUCT_SIGNAL_SINK_REPO;
|
|
64
|
+
return {
|
|
65
|
+
enabled,
|
|
66
|
+
sinkRepo,
|
|
67
|
+
source: "typed",
|
|
68
|
+
error: null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Resolve `plan.policy.productSignal` from PROJECT-DEFINITION (#2693). */
|
|
72
|
+
export function resolveProductSignal(projectRoot) {
|
|
73
|
+
const [data, err] = loadProjectDefinition(projectRoot);
|
|
74
|
+
if (data === null) {
|
|
75
|
+
return defaultResolved("default-on-error", err);
|
|
76
|
+
}
|
|
77
|
+
const policyBlock = readPlanPolicy(data.plan);
|
|
78
|
+
if (typeof policyBlock !== "object" ||
|
|
79
|
+
policyBlock === null ||
|
|
80
|
+
Array.isArray(policyBlock) ||
|
|
81
|
+
!("productSignal" in policyBlock)) {
|
|
82
|
+
return defaultResolved("default");
|
|
83
|
+
}
|
|
84
|
+
return resolveFromPolicyBlock(policyBlock.productSignal);
|
|
85
|
+
}
|
|
86
|
+
/** Human-readable status line for CLI surfaces. */
|
|
87
|
+
export function formatProductSignalStatusLine(policy) {
|
|
88
|
+
return (`[deft policy] productSignal enabled=${String(policy.enabled)} ` +
|
|
89
|
+
`sinkRepo=${policy.sinkRepo}.`);
|
|
90
|
+
}
|
|
91
|
+
function fieldFromResolved(resolved) {
|
|
92
|
+
return {
|
|
93
|
+
name: FIELD_PRODUCT_SIGNAL,
|
|
94
|
+
current: {
|
|
95
|
+
enabled: resolved.enabled,
|
|
96
|
+
sinkRepo: resolved.sinkRepo,
|
|
97
|
+
},
|
|
98
|
+
default: {
|
|
99
|
+
enabled: DEFAULT_PRODUCT_SIGNAL_ENABLED,
|
|
100
|
+
sinkRepo: DEFAULT_PRODUCT_SIGNAL_SINK_REPO,
|
|
101
|
+
},
|
|
102
|
+
source: resolved.source,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Inspector row for `policy:show --field=productSignal`. */
|
|
106
|
+
export function inspectProductSignal(data, projectRoot) {
|
|
107
|
+
if (data === null) {
|
|
108
|
+
return fieldFromResolved(defaultResolved("default"));
|
|
109
|
+
}
|
|
110
|
+
const policyBlock = readPlanPolicy(data.plan);
|
|
111
|
+
if (typeof policyBlock !== "object" ||
|
|
112
|
+
policyBlock === null ||
|
|
113
|
+
Array.isArray(policyBlock) ||
|
|
114
|
+
!("productSignal" in policyBlock)) {
|
|
115
|
+
if (projectRoot !== undefined) {
|
|
116
|
+
return fieldFromResolved(resolveProductSignal(projectRoot));
|
|
117
|
+
}
|
|
118
|
+
return fieldFromResolved(defaultResolved("default"));
|
|
119
|
+
}
|
|
120
|
+
return fieldFromResolved(resolveFromPolicyBlock(policyBlock.productSignal));
|
|
121
|
+
}
|
|
122
|
+
/** Persist `productSignal.enabled=true` after capability-cost disclosure (#2693). */
|
|
123
|
+
export function enableProductSignal(projectRoot, options) {
|
|
124
|
+
if (!options.confirm) {
|
|
125
|
+
return {
|
|
126
|
+
exitCode: 1,
|
|
127
|
+
stdout: `${PRODUCT_SIGNAL_CAPABILITY_COST_DISCLOSURE}\n\n` +
|
|
128
|
+
`Re-run with --confirm to apply: task product-signal:enable -- --confirm\n`,
|
|
129
|
+
changed: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const path = projectDefinitionPath(projectRoot);
|
|
133
|
+
try {
|
|
134
|
+
const { changed } = projectDefinitionMutationLock(projectRoot, () => {
|
|
135
|
+
const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
|
136
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
137
|
+
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
138
|
+
}
|
|
139
|
+
const data = parsed;
|
|
140
|
+
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
141
|
+
if (data.plan === undefined) {
|
|
142
|
+
data.plan = {};
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
throw new Error("PROJECT-DEFINITION 'plan' is not an object");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const plan = data.plan;
|
|
149
|
+
migrateLegacyPolicyKey(plan);
|
|
150
|
+
const existingPolicy = plan[PLAN_POLICY_KEY];
|
|
151
|
+
if (typeof existingPolicy !== "object" ||
|
|
152
|
+
existingPolicy === null ||
|
|
153
|
+
Array.isArray(existingPolicy)) {
|
|
154
|
+
if (existingPolicy === undefined) {
|
|
155
|
+
plan[PLAN_POLICY_KEY] = {};
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
throw new Error("plan.policy is not an object");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const policyBlock = plan[PLAN_POLICY_KEY];
|
|
162
|
+
const previous = policyBlock.productSignal;
|
|
163
|
+
const prevObj = typeof previous === "object" && previous !== null && !Array.isArray(previous)
|
|
164
|
+
? previous
|
|
165
|
+
: {};
|
|
166
|
+
const sinkRepo = options.sinkRepo?.trim() ||
|
|
167
|
+
(typeof prevObj.sinkRepo === "string" && prevObj.sinkRepo.trim().length > 0
|
|
168
|
+
? prevObj.sinkRepo.trim()
|
|
169
|
+
: DEFAULT_PRODUCT_SIGNAL_SINK_REPO);
|
|
170
|
+
const nextBlock = { enabled: true, sinkRepo };
|
|
171
|
+
const previousNormalized = resolveFromPolicyBlock(previous);
|
|
172
|
+
const changedFlag = previousNormalized.enabled !== nextBlock.enabled ||
|
|
173
|
+
previousNormalized.sinkRepo !== nextBlock.sinkRepo;
|
|
174
|
+
policyBlock.productSignal = nextBlock;
|
|
175
|
+
if (changedFlag) {
|
|
176
|
+
atomicWriteProjectDefinition(path, data);
|
|
177
|
+
}
|
|
178
|
+
const actor = options.actor ?? "task product-signal:enable";
|
|
179
|
+
const note = options.note ?? "";
|
|
180
|
+
const parts = [
|
|
181
|
+
`actor=${actor}`,
|
|
182
|
+
"productSignal.enabled=true",
|
|
183
|
+
`sinkRepo=${sinkRepo}`,
|
|
184
|
+
`previous=${JSON.stringify(previous ?? null)}`,
|
|
185
|
+
];
|
|
186
|
+
if (note) {
|
|
187
|
+
parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
|
|
188
|
+
}
|
|
189
|
+
appendAuditLog(projectRoot, parts.join(" "));
|
|
190
|
+
return { changed: changedFlag };
|
|
191
|
+
});
|
|
192
|
+
const resolved = resolveProductSignal(projectRoot);
|
|
193
|
+
const lines = [
|
|
194
|
+
`\u2713 ${FIELD_PRODUCT_SIGNAL}.enabled=true (product-signal ON).`,
|
|
195
|
+
changed
|
|
196
|
+
? " audit: meta/policy-changes.log updated."
|
|
197
|
+
: " no-op: value already matched (audit entry still appended for trail).",
|
|
198
|
+
formatProductSignalStatusLine(resolved),
|
|
199
|
+
];
|
|
200
|
+
return { exitCode: 0, stdout: `${lines.join("\n")}\n`, changed };
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
204
|
+
if (message.includes("PROJECT-DEFINITION not found")) {
|
|
205
|
+
return { exitCode: 2, stdout: `\u274c ${message}\n`, changed: false };
|
|
206
|
+
}
|
|
207
|
+
return { exitCode: 2, stdout: `\u274c Config error: ${message}\n`, changed: false };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=product-signal.js.map
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { type SemanticGreenFn } from "./semantic-green.js";
|
|
1
2
|
import type { MergeFn, MonitorFn, ProtectedCheckFn, WaitMergeableResult } from "./types.js";
|
|
2
3
|
export interface WaitMergeableOptions {
|
|
3
4
|
readonly protectedFn?: ProtectedCheckFn;
|
|
4
5
|
readonly monitorFn?: MonitorFn;
|
|
5
6
|
readonly mergeFn?: MergeFn;
|
|
7
|
+
readonly semanticGreenFn?: SemanticGreenFn;
|
|
8
|
+
/** Enable merge-cascade semantic-green gate (#2385). */
|
|
9
|
+
readonly cascadeMode?: boolean;
|
|
10
|
+
/** After a prior cascade merge, require target-branch CI green at HEAD. */
|
|
11
|
+
readonly requireMasterCiGreen?: boolean;
|
|
12
|
+
/** Target branch override for stale-base comparison (defaults to PR base.ref). */
|
|
13
|
+
readonly baseBranch?: string | null;
|
|
6
14
|
}
|
|
7
15
|
/** Run protected-check -> wait -> merge cascade (#1369). */
|
|
8
16
|
export declare function waitMergeableAndMerge(prNumber: number, repo: string, options: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { classifyMonitorOutcome, parseMonitorPayload } from "./classify.js";
|
|
2
2
|
import { EXIT_CONFIG_ERROR, EXIT_MERGED, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
|
|
3
3
|
import { makeResult } from "./result.js";
|
|
4
|
+
import { evaluateSemanticGreen } from "./semantic-green.js";
|
|
4
5
|
import { runGhMerge, runMonitor, runProtectedCheck } from "./wrappers.js";
|
|
5
6
|
/** Node module-not-found / missing script — not a protected-issue overlap (#2667). */
|
|
6
7
|
function isProtectedCheckConfigFailure(stderr) {
|
|
@@ -14,6 +15,7 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
|
|
|
14
15
|
const protectedFn = options.protectedFn ?? runProtectedCheck;
|
|
15
16
|
const monitorFn = options.monitorFn ?? runMonitor;
|
|
16
17
|
const mergeFn = options.mergeFn ?? runGhMerge;
|
|
18
|
+
const semanticGreenFn = options.semanticGreenFn ?? evaluateSemanticGreen;
|
|
17
19
|
const protectedIssues = options.protected;
|
|
18
20
|
let protectedCheckPayload = {};
|
|
19
21
|
if (protectedIssues.length > 0) {
|
|
@@ -47,6 +49,22 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
|
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
}
|
|
52
|
+
const semanticGreen = semanticGreenFn(prNumber, repo, {
|
|
53
|
+
cascadeMode: options.cascadeMode,
|
|
54
|
+
requireMasterCiGreen: options.requireMasterCiGreen,
|
|
55
|
+
baseBranch: options.baseBranch,
|
|
56
|
+
});
|
|
57
|
+
if (!semanticGreen.ok) {
|
|
58
|
+
return makeResult({
|
|
59
|
+
prNumber,
|
|
60
|
+
repo,
|
|
61
|
+
outcome: semanticGreen.outcome ?? "semantic-green-blocked",
|
|
62
|
+
exitCode: semanticGreen.exitCode ?? EXIT_TIMEOUT_OR_ESCALATION,
|
|
63
|
+
protectedCheck: protectedCheckPayload,
|
|
64
|
+
semanticGreen: { ...semanticGreen.payload },
|
|
65
|
+
error: semanticGreen.error,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
50
68
|
const [monRc, monStdout, monStderr] = monitorFn(prNumber, repo, options.capMinutes);
|
|
51
69
|
const monitorPayload = parseMonitorPayload(monStdout);
|
|
52
70
|
const [outcome, monitorExit] = classifyMonitorOutcome(monRc, monitorPayload);
|
|
@@ -5,6 +5,9 @@ export interface ParsedWaitMergeableArgs {
|
|
|
5
5
|
readonly capMinutes: number;
|
|
6
6
|
readonly protectedValues: readonly string[];
|
|
7
7
|
readonly emitJson: boolean;
|
|
8
|
+
readonly cascadeMode: boolean;
|
|
9
|
+
readonly requireMasterCiGreen: boolean;
|
|
10
|
+
readonly baseBranch: string | null;
|
|
8
11
|
readonly error?: string;
|
|
9
12
|
}
|
|
10
13
|
export declare function parseWaitMergeableArgs(argv: readonly string[]): ParsedWaitMergeableArgs;
|
|
@@ -12,6 +15,7 @@ export interface RunWaitMergeableOptions {
|
|
|
12
15
|
readonly protectedFn?: Parameters<typeof waitMergeableAndMerge>[2]["protectedFn"];
|
|
13
16
|
readonly monitorFn?: Parameters<typeof waitMergeableAndMerge>[2]["monitorFn"];
|
|
14
17
|
readonly mergeFn?: Parameters<typeof waitMergeableAndMerge>[2]["mergeFn"];
|
|
18
|
+
readonly semanticGreenFn?: Parameters<typeof waitMergeableAndMerge>[2]["semanticGreenFn"];
|
|
15
19
|
}
|
|
16
20
|
export declare function runWaitMergeable(argv: readonly string[], options?: RunWaitMergeableOptions): number;
|
|
17
21
|
export declare function cmdPrWaitMergeable(argv: readonly string[], options?: RunWaitMergeableOptions): number;
|
|
@@ -16,22 +16,45 @@ export function parseWaitMergeableArgs(argv) {
|
|
|
16
16
|
let capMinutes = 60;
|
|
17
17
|
const protectedValues = [];
|
|
18
18
|
let emitJson = false;
|
|
19
|
+
let cascadeMode = false;
|
|
20
|
+
let requireMasterCiGreen = false;
|
|
21
|
+
let baseBranch = null;
|
|
22
|
+
const baseReturn = () => ({
|
|
23
|
+
prNumber,
|
|
24
|
+
repo,
|
|
25
|
+
capMinutes,
|
|
26
|
+
protectedValues,
|
|
27
|
+
emitJson,
|
|
28
|
+
cascadeMode,
|
|
29
|
+
requireMasterCiGreen,
|
|
30
|
+
baseBranch,
|
|
31
|
+
});
|
|
19
32
|
for (let i = 0; i < argv.length; i += 1) {
|
|
20
33
|
const arg = argv[i];
|
|
21
34
|
if (arg === "--json") {
|
|
22
35
|
emitJson = true;
|
|
23
36
|
}
|
|
37
|
+
else if (arg === "--cascade") {
|
|
38
|
+
cascadeMode = true;
|
|
39
|
+
}
|
|
40
|
+
else if (arg === "--require-master-ci-green") {
|
|
41
|
+
requireMasterCiGreen = true;
|
|
42
|
+
}
|
|
43
|
+
else if (arg === "--base-branch") {
|
|
44
|
+
const value = argv[i + 1];
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
return { ...baseReturn(), error: "argument --base-branch: expected one argument" };
|
|
47
|
+
}
|
|
48
|
+
baseBranch = value;
|
|
49
|
+
i += 1;
|
|
50
|
+
}
|
|
51
|
+
else if (arg?.startsWith("--base-branch=")) {
|
|
52
|
+
baseBranch = arg.slice("--base-branch=".length);
|
|
53
|
+
}
|
|
24
54
|
else if (arg === "--repo") {
|
|
25
55
|
const value = argv[i + 1];
|
|
26
56
|
if (value === undefined) {
|
|
27
|
-
return {
|
|
28
|
-
prNumber,
|
|
29
|
-
repo,
|
|
30
|
-
capMinutes,
|
|
31
|
-
protectedValues,
|
|
32
|
-
emitJson,
|
|
33
|
-
error: "argument --repo: expected one argument",
|
|
34
|
-
};
|
|
57
|
+
return { ...baseReturn(), error: "argument --repo: expected one argument" };
|
|
35
58
|
}
|
|
36
59
|
repo = value;
|
|
37
60
|
i += 1;
|
|
@@ -42,25 +65,11 @@ export function parseWaitMergeableArgs(argv) {
|
|
|
42
65
|
else if (arg === "--cap-minutes") {
|
|
43
66
|
const value = argv[i + 1];
|
|
44
67
|
if (value === undefined) {
|
|
45
|
-
return {
|
|
46
|
-
prNumber,
|
|
47
|
-
repo,
|
|
48
|
-
capMinutes,
|
|
49
|
-
protectedValues,
|
|
50
|
-
emitJson,
|
|
51
|
-
error: "argument --cap-minutes: expected one argument",
|
|
52
|
-
};
|
|
68
|
+
return { ...baseReturn(), error: "argument --cap-minutes: expected one argument" };
|
|
53
69
|
}
|
|
54
70
|
const parsed = Number(value);
|
|
55
71
|
if (!Number.isFinite(parsed)) {
|
|
56
|
-
return {
|
|
57
|
-
prNumber,
|
|
58
|
-
repo,
|
|
59
|
-
capMinutes,
|
|
60
|
-
protectedValues,
|
|
61
|
-
emitJson,
|
|
62
|
-
error: `invalid --cap-minutes value: ${value}`,
|
|
63
|
-
};
|
|
72
|
+
return { ...baseReturn(), error: `invalid --cap-minutes value: ${value}` };
|
|
64
73
|
}
|
|
65
74
|
capMinutes = parsed;
|
|
66
75
|
i += 1;
|
|
@@ -69,28 +78,14 @@ export function parseWaitMergeableArgs(argv) {
|
|
|
69
78
|
const value = arg.slice("--cap-minutes=".length);
|
|
70
79
|
const parsed = Number(value);
|
|
71
80
|
if (!Number.isFinite(parsed)) {
|
|
72
|
-
return {
|
|
73
|
-
prNumber,
|
|
74
|
-
repo,
|
|
75
|
-
capMinutes,
|
|
76
|
-
protectedValues,
|
|
77
|
-
emitJson,
|
|
78
|
-
error: `invalid --cap-minutes value: ${value}`,
|
|
79
|
-
};
|
|
81
|
+
return { ...baseReturn(), error: `invalid --cap-minutes value: ${value}` };
|
|
80
82
|
}
|
|
81
83
|
capMinutes = parsed;
|
|
82
84
|
}
|
|
83
85
|
else if (arg === "--protected") {
|
|
84
86
|
const value = argv[i + 1];
|
|
85
87
|
if (value === undefined) {
|
|
86
|
-
return {
|
|
87
|
-
prNumber,
|
|
88
|
-
repo,
|
|
89
|
-
capMinutes,
|
|
90
|
-
protectedValues,
|
|
91
|
-
emitJson,
|
|
92
|
-
error: "argument --protected: expected one argument",
|
|
93
|
-
};
|
|
88
|
+
return { ...baseReturn(), error: "argument --protected: expected one argument" };
|
|
94
89
|
}
|
|
95
90
|
protectedValues.push(value);
|
|
96
91
|
i += 1;
|
|
@@ -99,51 +94,23 @@ export function parseWaitMergeableArgs(argv) {
|
|
|
99
94
|
protectedValues.push(arg.slice("--protected=".length));
|
|
100
95
|
}
|
|
101
96
|
else if (arg?.startsWith("-")) {
|
|
102
|
-
return {
|
|
103
|
-
prNumber,
|
|
104
|
-
repo,
|
|
105
|
-
capMinutes,
|
|
106
|
-
protectedValues,
|
|
107
|
-
emitJson,
|
|
108
|
-
error: `unrecognized arguments: ${arg}`,
|
|
109
|
-
};
|
|
97
|
+
return { ...baseReturn(), error: `unrecognized arguments: ${arg}` };
|
|
110
98
|
}
|
|
111
99
|
else if (prNumber === null) {
|
|
112
100
|
const n = Number(arg);
|
|
113
101
|
if (!Number.isInteger(n)) {
|
|
114
|
-
return {
|
|
115
|
-
prNumber,
|
|
116
|
-
repo,
|
|
117
|
-
capMinutes,
|
|
118
|
-
protectedValues,
|
|
119
|
-
emitJson,
|
|
120
|
-
error: `invalid PR number: ${arg}`,
|
|
121
|
-
};
|
|
102
|
+
return { ...baseReturn(), error: `invalid PR number: ${arg}` };
|
|
122
103
|
}
|
|
123
104
|
prNumber = n;
|
|
124
105
|
}
|
|
125
106
|
else {
|
|
126
|
-
return {
|
|
127
|
-
prNumber,
|
|
128
|
-
repo,
|
|
129
|
-
capMinutes,
|
|
130
|
-
protectedValues,
|
|
131
|
-
emitJson,
|
|
132
|
-
error: `unrecognized arguments: ${arg}`,
|
|
133
|
-
};
|
|
107
|
+
return { ...baseReturn(), error: `unrecognized arguments: ${arg}` };
|
|
134
108
|
}
|
|
135
109
|
}
|
|
136
110
|
if (prNumber === null) {
|
|
137
|
-
return {
|
|
138
|
-
prNumber,
|
|
139
|
-
repo,
|
|
140
|
-
capMinutes,
|
|
141
|
-
protectedValues,
|
|
142
|
-
emitJson,
|
|
143
|
-
error: "the following arguments are required: pr_number",
|
|
144
|
-
};
|
|
111
|
+
return { ...baseReturn(), error: "the following arguments are required: pr_number" };
|
|
145
112
|
}
|
|
146
|
-
return
|
|
113
|
+
return baseReturn();
|
|
147
114
|
}
|
|
148
115
|
function summaryLabelForExit(exitCode) {
|
|
149
116
|
switch (exitCode) {
|
|
@@ -183,6 +150,10 @@ export function runWaitMergeable(argv, options = {}) {
|
|
|
183
150
|
protectedFn: options.protectedFn,
|
|
184
151
|
monitorFn: options.monitorFn,
|
|
185
152
|
mergeFn: options.mergeFn,
|
|
153
|
+
semanticGreenFn: options.semanticGreenFn,
|
|
154
|
+
cascadeMode: args.cascadeMode,
|
|
155
|
+
requireMasterCiGreen: args.requireMasterCiGreen,
|
|
156
|
+
baseBranch: args.baseBranch,
|
|
186
157
|
});
|
|
187
158
|
const summaryLabel = summaryLabelForExit(result.exitCode);
|
|
188
159
|
process.stderr.write(`[pr_wait_mergeable] PR #${result.prNumber} repo=${result.repo} ` +
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { WaitMergeableResult } from "./types.js";
|
|
2
2
|
export declare function toResultDict(result: WaitMergeableResult): Record<string, unknown>;
|
|
3
|
-
export declare function makeResult(fields: Omit<WaitMergeableResult, "monitorResult" | "protectedCheck" | "mergeStdout" | "mergeStderr"> & {
|
|
3
|
+
export declare function makeResult(fields: Omit<WaitMergeableResult, "monitorResult" | "protectedCheck" | "semanticGreen" | "mergeStdout" | "mergeStderr"> & {
|
|
4
4
|
readonly monitorResult?: Record<string, unknown>;
|
|
5
5
|
readonly protectedCheck?: Record<string, unknown>;
|
|
6
|
+
readonly semanticGreen?: Record<string, unknown>;
|
|
6
7
|
readonly mergeStdout?: string;
|
|
7
8
|
readonly mergeStderr?: string;
|
|
8
9
|
}): WaitMergeableResult;
|
|
@@ -11,6 +11,9 @@ export function toResultDict(result) {
|
|
|
11
11
|
if (Object.keys(result.protectedCheck).length > 0) {
|
|
12
12
|
payload.protected_check = result.protectedCheck;
|
|
13
13
|
}
|
|
14
|
+
if (Object.keys(result.semanticGreen).length > 0) {
|
|
15
|
+
payload.semantic_green = result.semanticGreen;
|
|
16
|
+
}
|
|
14
17
|
if (result.mergeStdout.length > 0) {
|
|
15
18
|
payload.merge_stdout = result.mergeStdout;
|
|
16
19
|
}
|
|
@@ -26,6 +29,7 @@ export function makeResult(fields) {
|
|
|
26
29
|
return {
|
|
27
30
|
monitorResult: fields.monitorResult ?? {},
|
|
28
31
|
protectedCheck: fields.protectedCheck ?? {},
|
|
32
|
+
semanticGreen: fields.semanticGreen ?? {},
|
|
29
33
|
mergeStdout: fields.mergeStdout ?? "",
|
|
30
34
|
mergeStderr: fields.mergeStderr ?? "",
|
|
31
35
|
prNumber: fields.prNumber,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RunGhFn } from "../pr-merge-readiness/types.js";
|
|
2
|
+
export interface SemanticGreenOptions {
|
|
3
|
+
/** Enable merge-cascade semantic-green checks (#2385). */
|
|
4
|
+
readonly cascadeMode?: boolean;
|
|
5
|
+
/** After a prior cascade merge, require target-branch CI green at HEAD before the next merge. */
|
|
6
|
+
readonly requireMasterCiGreen?: boolean;
|
|
7
|
+
/** Target branch for stale-base comparison (defaults to PR base.ref). */
|
|
8
|
+
readonly baseBranch?: string | null;
|
|
9
|
+
readonly runGh?: RunGhFn;
|
|
10
|
+
}
|
|
11
|
+
export interface SemanticGreenPayload {
|
|
12
|
+
readonly pr_base_sha: string | null;
|
|
13
|
+
readonly target_branch: string | null;
|
|
14
|
+
readonly target_head_sha: string | null;
|
|
15
|
+
readonly master_ci: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface SemanticGreenResult {
|
|
18
|
+
readonly ok: boolean;
|
|
19
|
+
readonly outcome: string | null;
|
|
20
|
+
readonly exitCode: number | null;
|
|
21
|
+
readonly error: string | null;
|
|
22
|
+
readonly payload: SemanticGreenPayload;
|
|
23
|
+
}
|
|
24
|
+
/** Pre-merge semantic-green gate for merge cascades (#2385). */
|
|
25
|
+
export declare function evaluateSemanticGreen(prNumber: number, repo: string, options?: SemanticGreenOptions): SemanticGreenResult;
|
|
26
|
+
export type SemanticGreenFn = typeof evaluateSemanticGreen;
|
|
27
|
+
//# sourceMappingURL=semantic-green.d.ts.map
|