feature-factory 0.10.4 → 0.10.6
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/README.md +13 -7
- package/WORKFLOW.md +34 -39
- package/bin/factory.js +17 -2
- package/observe/identity.js +44 -0
- package/observe/repository-config.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,11 +47,13 @@ resolved Git top level:
|
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
The root has two required properties, `resolve` and `verify`, and
|
|
51
|
-
`pr_draft`, `verify_timeout_ms`, `bootstrap`, and `
|
|
50
|
+
The root has two required properties, `resolve` and `verify`, and six optional properties: `publish`,
|
|
51
|
+
`pr_draft`, `verify_timeout_ms`, `bootstrap`, `bootstrap_timeout_ms`, and `max_retries`. Required commands and any present
|
|
52
52
|
`publish` or `bootstrap` are non-empty strings. There is no `publishing_identity` key, and a file carrying one is malformed because the
|
|
53
53
|
optional set is closed. A present `pr_draft` must be a JSON boolean and omission means `true`. Both timeouts are
|
|
54
54
|
positive safe integers. `bootstrap_timeout_ms` requires `bootstrap`.
|
|
55
|
+
A present `max_retries` must be a positive integer. It is the run's attempt budget when `init` runs
|
|
56
|
+
without `--max-retries`, which still outranks it; omission leaves the default `3`.
|
|
55
57
|
Each omitted timeout independently defaults to `900000`; neither shares the other's budget. The file is
|
|
56
58
|
operator-owned, committed, and protected as a privileged path: a run cannot create, write, merge,
|
|
57
59
|
archive, package, or repair it.
|
|
@@ -66,9 +68,9 @@ value stops the run instead of publishing under whatever credential the host hap
|
|
|
66
68
|
it from `gh`, the token, stored authentication, or Git configuration: an expectation read from the
|
|
67
69
|
credential being checked would always match.
|
|
68
70
|
|
|
69
|
-
Validation refuses the first matching defect in this order: unreadable or invalid JSON, a non-object root, or unknown keys; invalid `pr_draft`; invalid `bootstrap`; `bootstrap_timeout_ms` without `bootstrap`; invalid `bootstrap_timeout_ms`; invalid `verify_timeout_ms`; missing or invalid required entries; then invalid `
|
|
71
|
+
Validation refuses the first matching defect in this order: unreadable or invalid JSON, a non-object root, or unknown keys; invalid `pr_draft`; invalid `bootstrap`; `bootstrap_timeout_ms` without `bootstrap`; invalid `bootstrap_timeout_ms`; invalid `verify_timeout_ms`; missing or invalid required entries; invalid `publish`; then invalid `max_retries`.
|
|
70
72
|
|
|
71
|
-
The named forms are `.factory.json entry 'pr_draft' must be a boolean`, `.factory.json entry 'bootstrap' must be a non-empty string`, `.factory.json entry 'bootstrap_timeout_ms' requires a declared bootstrap command`, `.factory.json entry 'bootstrap_timeout_ms' must be a positive integer`, and `.factory.json entry '
|
|
73
|
+
The named forms are `.factory.json entry 'pr_draft' must be a boolean`, `.factory.json entry 'bootstrap' must be a non-empty string`, `.factory.json entry 'bootstrap_timeout_ms' requires a declared bootstrap command`, `.factory.json entry 'bootstrap_timeout_ms' must be a positive integer`, `.factory.json entry 'verify_timeout_ms' must be a positive integer`, and `.factory.json entry 'max_retries' must be a positive integer`.
|
|
72
74
|
|
|
73
75
|
A present invalid, unreadable, incomplete, wrong-type, whitespace-only, or unknown-property config refuses closed.
|
|
74
76
|
|
|
@@ -287,14 +289,18 @@ report `null`, skips all three guards; an absent config does not affect them.
|
|
|
287
289
|
|
|
288
290
|
Before each guard, inherited `GH_TOKEN` must exist and contain at least one character. Missing or empty
|
|
289
291
|
means identity is unobservable without invoking `gh`, the network, stored authentication, credential
|
|
290
|
-
queries, or any fallback. A prepared environment submits exactly this
|
|
292
|
+
queries, or any fallback. A prepared environment submits exactly this command as one
|
|
291
293
|
ordinary host shell step with cwd exactly `RUN_REPO`, inherited environment, and no stdin:
|
|
292
294
|
|
|
293
295
|
```sh
|
|
294
|
-
|
|
296
|
+
factory identity "$R" --json --repo "$RUN_REPO"
|
|
295
297
|
```
|
|
296
298
|
|
|
297
|
-
The
|
|
299
|
+
The CLI runs the read-only network probe `gh api --method GET /user --jq .login` itself, with separate
|
|
300
|
+
stdout and stderr pipes, so a host whose shell tool combines the two streams can still run the guard. It
|
|
301
|
+
returns JSON whose `reason` is null on a match and is otherwise the complete rendered park reason.
|
|
302
|
+
|
|
303
|
+
The probe's stdout bytes, stderr bytes, and numeric status are parsed strictly. Only numeric zero, empty
|
|
298
304
|
stderr, and exactly one ASCII GitHub login plus one LF are observable; the required LF alone is removed,
|
|
299
305
|
then the raw login is compared exactly and case-sensitively with the raw declaration. `gh auth status`
|
|
300
306
|
does not prove the publishing identity.
|
package/WORKFLOW.md
CHANGED
|
@@ -547,8 +547,8 @@ The optional repository-owned file is `$O/.factory.json`:
|
|
|
547
547
|
```
|
|
548
548
|
|
|
549
549
|
The root must be a JSON object with the two required own properties `resolve` and `verify`,
|
|
550
|
-
plus only the optional own properties `publish`, `pr_draft`, `verify_timeout_ms`, `bootstrap`,
|
|
551
|
-
`bootstrap_timeout_ms`. `resolve`, `verify`, `publish`, and `bootstrap` are command strings; every present
|
|
550
|
+
plus only the optional own properties `publish`, `pr_draft`, `verify_timeout_ms`, `bootstrap`,
|
|
551
|
+
`bootstrap_timeout_ms`, and `max_retries`. `resolve`, `verify`, `publish`, and `bootstrap` are command strings; every present
|
|
552
552
|
command must be non-empty. `publish` was required and invoked nowhere until this release, so every
|
|
553
553
|
consumer wrote a command that could not run. It is optional now and contributes only the file candidate
|
|
554
554
|
to the one Step 6 publishing selection. Inherited `FACTORY_PUBLISHING_COMMAND` or the default may win, so
|
|
@@ -559,9 +559,10 @@ key is malformed, because the optional set above is closed. `pr_draft` must be a
|
|
|
559
559
|
when present and defaults to `true` when absent. Both timeout values must be positive
|
|
560
560
|
safe integers when present, and `bootstrap_timeout_ms` is valid only with a declared `bootstrap`.
|
|
561
561
|
`verify_timeout_ms` and `bootstrap_timeout_ms` each independently default to `900000` milliseconds;
|
|
562
|
-
neither timeout shares or consumes the other's budget.
|
|
562
|
+
neither timeout shares or consumes the other's budget. A present `max_retries` must be a positive integer. It is the run's attempt budget when `init` runs
|
|
563
|
+
without `--max-retries`, which still outranks it; omission leaves the default `3`.
|
|
563
564
|
|
|
564
|
-
Validation refuses the first matching defect in this order: unreadable or invalid JSON, a non-object root, or unknown keys; invalid `pr_draft`; invalid `bootstrap`; `bootstrap_timeout_ms` without `bootstrap`; invalid `bootstrap_timeout_ms`; invalid `verify_timeout_ms`; missing or invalid required entries; then invalid `
|
|
565
|
+
Validation refuses the first matching defect in this order: unreadable or invalid JSON, a non-object root, or unknown keys; invalid `pr_draft`; invalid `bootstrap`; `bootstrap_timeout_ms` without `bootstrap`; invalid `bootstrap_timeout_ms`; invalid `verify_timeout_ms`; missing or invalid required entries; invalid `publish`; then invalid `max_retries`.
|
|
565
566
|
|
|
566
567
|
Do not use the obsolete summary “Validation refuses the first matching defect in this order: unreadable or invalid JSON, a non-object root, or unknown keys; invalid `bootstrap`; `bootstrap_timeout_ms` without `bootstrap`; invalid `bootstrap_timeout_ms`; invalid `verify_timeout_ms`; then missing or invalid required entries.” because it omits the earlier `pr_draft` check.
|
|
567
568
|
|
|
@@ -908,6 +909,12 @@ with `git check-ref-format --branch "$FEATURE_BRANCH"`. Bind
|
|
|
908
909
|
`git -C "$O" show-ref --verify --quiet "$FEATURE_REF"` to exit exactly 1 for ref absence. Exit 0 means
|
|
909
910
|
present and every other result is a lookup error; either refuses before init.
|
|
910
911
|
|
|
912
|
+
Bind `KEY` from how `R` was bound, never from anything else: when the configured resolver returned a
|
|
913
|
+
payload, `KEY` is exactly `R`, its `run_id`; when `R` is a ticket key from the request or branch, `KEY`
|
|
914
|
+
is that key; when `R` was derived from free text, `KEY` is absent and `--issue` is omitted. Never add a
|
|
915
|
+
`#` or other decoration. `issue_key` is immutable after init, and publication reads it for the title
|
|
916
|
+
prefix and closing reference, so a run whose resolver found issue `25` must record `25`.
|
|
917
|
+
|
|
911
918
|
An explicit `PR_BASE` wins. Otherwise require the symbolic branch in the configured operator worktree;
|
|
912
919
|
detached, missing, escaping, or unprovable worktree state is refused by init. Request one fresh sandbox
|
|
913
920
|
by running exactly the command below, command first, including each bracketed flag only when its value
|
|
@@ -1073,53 +1080,41 @@ reconciliation, reading `status.next`, dispatch, or any transition. For a parked
|
|
|
1073
1080
|
immediately after explicit resume has been verified `running` with unchanged historical result, real
|
|
1074
1081
|
next action, and the same fresh owner. No operation may intervene on either side of this guard.
|
|
1075
1082
|
|
|
1076
|
-
At every one of the three guards,
|
|
1077
|
-
|
|
1078
|
-
`GH_TOKEN` is immediately the same unobservable reason below. Do not invoke `gh`, hit the network,
|
|
1079
|
-
inspect stored authentication, query or attempt credentials, or run any fallback in that case.
|
|
1080
|
-
|
|
1081
|
-
After that preflight succeeds, submit exactly this command as one ordinary host shell step with cwd
|
|
1082
|
-
exactly `RUN_REPO`, the inherited environment including that nonempty `GH_TOKEN`, and no stdin:
|
|
1083
|
+
At every one of the three guards, submit exactly this command as one ordinary host shell step with cwd
|
|
1084
|
+
exactly `RUN_REPO`, the inherited environment, and no stdin:
|
|
1083
1085
|
|
|
1084
1086
|
```sh
|
|
1085
|
-
|
|
1087
|
+
factory identity "$R" --json --repo "$RUN_REPO"
|
|
1086
1088
|
```
|
|
1087
1089
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
`^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$`. Every other status or byte sequence is unobservable;
|
|
1096
|
-
do not trim, decode-and-normalize, retry, or recover a partial value. Remove only the required final LF
|
|
1097
|
-
from an observable value, then compare the raw declared and observed strings exactly and
|
|
1098
|
-
case-sensitively before rendering either one.
|
|
1090
|
+
The CLI owns the observation, so the verdict does not depend on what the host's shell tool reports. It
|
|
1091
|
+
refuses without invoking `gh` when inherited `GH_TOKEN` is missing or empty. Otherwise it runs the
|
|
1092
|
+
read-only network observation `gh api --method GET /user --jq .login` with separate stdout and stderr pipes,
|
|
1093
|
+
accepts only numeric status zero, zero-byte stderr, and exactly one ASCII login matching
|
|
1094
|
+
`^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$` followed by one LF, and compares it exactly and
|
|
1095
|
+
case-sensitively with the recorded `publishing_identity`. A host whose shell tool combines stdout and
|
|
1096
|
+
stderr can run this guard; it could never run the probe itself.
|
|
1099
1097
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
U+0009, U+000A, U+000C, and U+000D. Render every other UTF-16 code unit outside U+0020 through U+007E
|
|
1104
|
-
as lowercase `\uXXXX`. A non-BMP code point therefore renders as its two surrogate units, and an
|
|
1105
|
-
unpaired surrogate renders as its one unit. Leave slash unescaped. This covers C0, C1, DEL, U+0085,
|
|
1106
|
-
U+2028, U+2029, and non-BMP input without a literal non-ASCII or control byte.
|
|
1107
|
-
|
|
1108
|
-
An observable unequal value uses exactly:
|
|
1098
|
+
Require exit zero and one JSON object. `reason: null` passes the guard. A non-null `reason` is the
|
|
1099
|
+
complete already-rendered ASCII park reason, rendered with deterministic ASCII-only JSON strings, and is
|
|
1100
|
+
exactly one of:
|
|
1109
1101
|
|
|
1110
1102
|
```text
|
|
1111
1103
|
publishing identity mismatch: declared <declared-ascii-json>, observed <observed-ascii-json>; authenticate as <declared-ascii-json> and retry.
|
|
1112
1104
|
```
|
|
1113
1105
|
|
|
1114
|
-
An unobservable result uses exactly:
|
|
1115
|
-
|
|
1116
1106
|
```text
|
|
1117
1107
|
publishing identity unobservable: declared <declared-ascii-json>; launch with inherited GH_TOKEN for <declared-ascii-json> as documented in OPERATING.md and retry.
|
|
1118
1108
|
```
|
|
1119
1109
|
|
|
1110
|
+
Use the returned `reason` exactly; do not re-render, trim, or edit it. Never run `gh` yourself for this
|
|
1111
|
+
guard, retry, fall back to stored authentication, or put a token in argv. A nonzero exit or output that is
|
|
1112
|
+
not that JSON object is a failed guard: perform no further operation and report only
|
|
1113
|
+
`Outcome: retained-lock-error`.
|
|
1114
|
+
|
|
1120
1115
|
Never expose the token, raw stdout or stderr, diagnostics, status, command text, target, helper output,
|
|
1121
1116
|
or environment. On either reason, quiesce every builder, tool, background task, and heartbeat call.
|
|
1122
|
-
Bind `PRE_QUOTING_REASON` to the complete already-rendered ASCII reason. Encode it as one deterministic
|
|
1117
|
+
Bind `PRE_QUOTING_REASON` to the complete already-rendered ASCII reason the CLI returned. Encode it as one deterministic
|
|
1123
1118
|
POSIX shell token by surrounding the complete reason with single quotes and replacing every literal
|
|
1124
1119
|
`'` inside it with the exact shell sequence `'\''`. Use that encoded token as the sole `--reason`
|
|
1125
1120
|
argument in the host shell command string:
|
|
@@ -2168,12 +2163,12 @@ this is not a claim that a target is unobservable. Use the same three exact reda
|
|
|
2168
2163
|
from Step 0.
|
|
2169
2164
|
|
|
2170
2165
|
With `DECLARED_PUBLISHING_IDENTITY`, immediately after exact target equality and before the unchanged
|
|
2171
|
-
push, run the same
|
|
2172
|
-
|
|
2166
|
+
push, run the same identity guard command under its exact cwd, environment, no-stdin, verdict,
|
|
2167
|
+
redaction, and parking rules. No operation may intervene between equality, this
|
|
2173
2168
|
guard, and the push:
|
|
2174
2169
|
|
|
2175
2170
|
```sh
|
|
2176
|
-
|
|
2171
|
+
factory identity "$R" --json --repo "$RUN_REPO"
|
|
2177
2172
|
```
|
|
2178
2173
|
|
|
2179
2174
|
Publish the fully qualified recorded feature ref from `RUN_REPO`, run `gh` from `O` with that exact head
|
|
@@ -2183,7 +2178,7 @@ legacy local runs use `O` through the selection already made in Step 0:
|
|
|
2183
2178
|
|
|
2184
2179
|
```sh
|
|
2185
2180
|
git -C "$RUN_REPO" push origin "refs/heads/$FEATURE_BRANCH:refs/heads/$FEATURE_BRANCH"
|
|
2186
|
-
|
|
2181
|
+
factory identity "$R" --json --repo "$RUN_REPO"
|
|
2187
2182
|
(
|
|
2188
2183
|
cd "$O"
|
|
2189
2184
|
if [ "$PR_DRAFT" = true ]; then
|
package/bin/factory.js
CHANGED
|
@@ -14,6 +14,7 @@ import { nextAction, nextActionRecord, readRun, readRunUnchecked } from "../stat
|
|
|
14
14
|
import { transition } from "../state/transition.js";
|
|
15
15
|
import { RUN_JSON_LOCK_DIR, withRunJsonLock } from "../core/run-lock.js";
|
|
16
16
|
import { buildEvidence, deriveReviewReady, EVIDENCE_KEYS, evidenceRef, git, observeAncestry, observeCleanliness, observeTrackedCleanliness, observeWorktree, privilegedPaths, proveInitContainment, resolveWorktree, runBootstrap, unownedPaths } from "../observe/index.js";
|
|
17
|
+
import { identityReason, observeIdentity } from "../observe/identity.js";
|
|
17
18
|
import { assertPublicationReady, assertReviewBinding, isApproving, observeMergeProof, readEvidence, readReview, readValidatorReview } from "../observe/review.js";
|
|
18
19
|
import { readRepositoryConfig, RepositoryConfigError } from "../observe/repository-config.js";
|
|
19
20
|
import { reverifyRepair } from "../observe/repair-reverification.js";
|
|
@@ -60,6 +61,7 @@ export const COMMANDS = Object.freeze({
|
|
|
60
61
|
pr: Object.freeze(["--repo", "--url", "--now", "--json"]),
|
|
61
62
|
"reverify-repair": Object.freeze(["--repo", "--now", "--json"]),
|
|
62
63
|
"effective-push": Object.freeze([]),
|
|
64
|
+
identity: Object.freeze(["--repo", "--json"]),
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
const BOOLEAN_FLAGS = new Set(["--json", "--repository-verify"]);
|
|
@@ -82,7 +84,7 @@ export async function run(argv) {
|
|
|
82
84
|
if (!Object.hasOwn(COMMANDS, command)) throw new CliError(`unknown command '${command}' (try --help)`);
|
|
83
85
|
const { positional, flags } = parse(command, rest);
|
|
84
86
|
const handler = HANDLERS[command];
|
|
85
|
-
if (["init", "status", "snapshot", "lock", "heartbeat", "effective-push"].includes(command)) return handler(positional, flags);
|
|
87
|
+
if (["init", "status", "snapshot", "lock", "heartbeat", "effective-push", "identity"].includes(command)) return handler(positional, flags);
|
|
86
88
|
const repo = resolve(flags.repo ?? process.cwd()), runId = positional[0];
|
|
87
89
|
if (command === "restore") {
|
|
88
90
|
const live = [join(repo, CONTROL_PLANE, runId), join(repo, ".factory-sandboxes", runId, CONTROL_PLANE, runId)]
|
|
@@ -1281,6 +1283,16 @@ const HANDLERS = {
|
|
|
1281
1283
|
throw new CliError("factory lock requires <claim|steal|release>");
|
|
1282
1284
|
},
|
|
1283
1285
|
|
|
1286
|
+
// Read-only: no lock, no state write. The driver parks with `reason` when it is non-null.
|
|
1287
|
+
async identity(positional, flags) {
|
|
1288
|
+
if (positional.length !== 1) throw new CliError("factory identity requires exactly <run-id>");
|
|
1289
|
+
const [runId] = positional, run = readRun(runDirFor(flags, runId)), declared = run.publishing_identity ?? null;
|
|
1290
|
+
if (declared === null) return emit(flags, { run_id: runId, publishing_identity: null, checked: false, reason: null });
|
|
1291
|
+
const observed = observeIdentity(resolve(flags.repo ?? process.cwd()));
|
|
1292
|
+
return emit(flags, { run_id: runId, publishing_identity: declared, checked: true, observable: observed !== null,
|
|
1293
|
+
reason: identityReason(declared, observed) });
|
|
1294
|
+
},
|
|
1295
|
+
|
|
1284
1296
|
async heartbeat([runId], flags) {
|
|
1285
1297
|
const runDir = runDirFor(flags, runId);
|
|
1286
1298
|
const owner = await refreshSessionLock(runDir, {
|
|
@@ -1727,7 +1739,9 @@ export async function dispatchInit(positional, flags, operations = INIT_OPERATIO
|
|
|
1727
1739
|
proveContainedBranch();
|
|
1728
1740
|
let run;
|
|
1729
1741
|
try {
|
|
1730
|
-
|
|
1742
|
+
// An explicit `--max-retries` outranks the committed `.factory.json` default, which outranks 3.
|
|
1743
|
+
const configuredRetries = flags.maxRetries === undefined && config?.maxRetries ? { max_retries: config.maxRetries } : {};
|
|
1744
|
+
run = validateRun({ ...candidate, ...configuredRetries, pr_base: prBase, pr_draft: config?.prDraft ?? true, ...bootstrapEvidence });
|
|
1731
1745
|
} catch (error) {
|
|
1732
1746
|
throw new CliError(`final manifest validation failed for sandbox '${S}'; sandbox was retained`, { cause: error });
|
|
1733
1747
|
}
|
|
@@ -1873,6 +1887,7 @@ function usage() {
|
|
|
1873
1887
|
factory resume <run-id> --session ID [--now ISO]
|
|
1874
1888
|
factory restore <run-id> --repo OPERATOR --from refs/remotes/REMOTE/BRANCH [--now ISO]
|
|
1875
1889
|
factory snapshot <run-id> --repo OPERATOR
|
|
1890
|
+
factory identity <run-id> [--repo PATH] [--json]
|
|
1876
1891
|
factory reverify-repair <run-id> <repair-record-id> [--repo PATH] [--now ISO] [--json]
|
|
1877
1892
|
factory lock <run-id> <claim|steal|release> --session ID [--ttl-ms N]
|
|
1878
1893
|
factory heartbeat <run-id> --session ID
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// Enforcement (false green): publishing under an account other than the run's recorded identity. This
|
|
4
|
+
// was driver prose that required the host to return stdout, stderr and status separately; Prime's bash
|
|
5
|
+
// returns them combined, so a compliant Prime driver could never observe the identity (#365). The CLI
|
|
6
|
+
// owns the pipes now, so the verdict no longer depends on what the host's shell tool can report.
|
|
7
|
+
export const IDENTITY_ARGV = Object.freeze(["api", "--method", "GET", "/user", "--jq", ".login"]);
|
|
8
|
+
const LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
|
|
9
|
+
const SHORT = new Map([[8, "\\b"], [9, "\\t"], [10, "\\n"], [12, "\\f"], [13, "\\r"]]);
|
|
10
|
+
|
|
11
|
+
// Deterministic ASCII-only JSON string: printable ASCII literal except quote and backslash, the fixed
|
|
12
|
+
// short escapes, and every other UTF-16 code unit as lowercase \uXXXX. Slash stays unescaped.
|
|
13
|
+
export function asciiJson(value) {
|
|
14
|
+
let rendered = "\"";
|
|
15
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
16
|
+
const unit = value.charCodeAt(index);
|
|
17
|
+
if (SHORT.has(unit)) rendered += SHORT.get(unit);
|
|
18
|
+
else if (unit === 34 || unit === 92) rendered += `\\${value[index]}`;
|
|
19
|
+
else if (unit >= 0x20 && unit <= 0x7e) rendered += value[index];
|
|
20
|
+
else rendered += `\\u${unit.toString(16).padStart(4, "0")}`;
|
|
21
|
+
}
|
|
22
|
+
return `${rendered}"`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Observable only as numeric zero, zero-byte stderr, and exactly one login plus one LF. Nothing is
|
|
26
|
+
// trimmed, retried, or recovered from a partial value.
|
|
27
|
+
export function classifyIdentity({ status, stdout, stderr }) {
|
|
28
|
+
if (status !== 0 || !Buffer.isBuffer(stdout) || !Buffer.isBuffer(stderr) || stderr.length !== 0) return null;
|
|
29
|
+
if (stdout.length < 2 || stdout[stdout.length - 1] !== 10 || stdout.subarray(0, -1).some((byte) => byte > 0x7f)) return null;
|
|
30
|
+
const login = stdout.subarray(0, -1).toString("ascii");
|
|
31
|
+
return LOGIN.test(login) ? login : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function identityReason(declared, observed) {
|
|
35
|
+
const name = asciiJson(declared);
|
|
36
|
+
if (observed === null) return `publishing identity unobservable: declared ${name}; launch with inherited GH_TOKEN for ${name} as documented in OPERATING.md and retry.`;
|
|
37
|
+
return observed === declared ? null : `publishing identity mismatch: declared ${name}, observed ${asciiJson(observed)}; authenticate as ${name} and retry.`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A missing or empty GH_TOKEN never reaches gh, the network, or stored authentication.
|
|
41
|
+
export function observeIdentity(cwd, env = process.env, timeout = 60000) {
|
|
42
|
+
if (typeof env.GH_TOKEN !== "string" || env.GH_TOKEN.length === 0) return null;
|
|
43
|
+
return classifyIdentity(spawnSync("gh", IDENTITY_ARGV, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout }));
|
|
44
|
+
}
|
|
@@ -21,7 +21,7 @@ export function parseRepositoryConfig(bytes) {
|
|
|
21
21
|
// not. It is optional now and consumed when present, so the key means what it says either way.
|
|
22
22
|
const requiredKeys = ["resolve", "verify"];
|
|
23
23
|
const optionalCommandKeys = ["publish"];
|
|
24
|
-
const allowedKeys = [...requiredKeys, ...optionalCommandKeys, "pr_draft", "verify_timeout_ms", "bootstrap", "bootstrap_timeout_ms"];
|
|
24
|
+
const allowedKeys = [...requiredKeys, ...optionalCommandKeys, "pr_draft", "verify_timeout_ms", "bootstrap", "bootstrap_timeout_ms", "max_retries"];
|
|
25
25
|
if (!config || typeof config !== "object" || Array.isArray(config)
|
|
26
26
|
|| Object.keys(config).some((keyName) => !allowedKeys.includes(keyName))) {
|
|
27
27
|
throw new RepositoryConfigError("invalid .factory.json");
|
|
@@ -53,8 +53,14 @@ export function parseRepositoryConfig(bytes) {
|
|
|
53
53
|
&& (typeof config[keyName] !== "string" || !config[keyName].trim()))) {
|
|
54
54
|
throw new RepositoryConfigError("invalid .factory.json: entry 'publish' must be a non-empty string");
|
|
55
55
|
}
|
|
56
|
+
// A project default for `init --max-retries`, so it need not ride on every invocation. Instruction, not
|
|
57
|
+
// enforcement: more attempts are more reviewed work, never an unearned merge. A present value is still
|
|
58
|
+
// checked, because a key that is silently ignored reads as a budget that took effect.
|
|
59
|
+
if (Object.hasOwn(config, "max_retries") && (!Number.isSafeInteger(config.max_retries) || config.max_retries <= 0)) {
|
|
60
|
+
throw new RepositoryConfigError("invalid .factory.json: entry 'max_retries' must be a positive integer");
|
|
61
|
+
}
|
|
56
62
|
const parsed = { command: config.verify, timeoutMs: config.verify_timeout_ms ?? DEFAULT_REPOSITORY_VERIFY_TIMEOUT_MS,
|
|
57
|
-
prDraft: config.pr_draft ?? true };
|
|
63
|
+
prDraft: config.pr_draft ?? true, ...(Object.hasOwn(config, "max_retries") ? { maxRetries: config.max_retries } : {}) };
|
|
58
64
|
return hasBootstrap ? { ...parsed, bootstrapCommand: config.bootstrap,
|
|
59
65
|
bootstrapTimeoutMs: config.bootstrap_timeout_ms ?? DEFAULT_BOOTSTRAP_TIMEOUT_MS } : parsed;
|
|
60
66
|
}
|