c8ctl-plugin-nano 1.35.6 → 1.36.1
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 +30 -5
- package/c8ctl-plugin.js +211 -30
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -485,7 +485,7 @@ stdin payload as `task`:
|
|
|
485
485
|
Element templates emit flat dotpath header keys (strings); the plugin expands
|
|
486
486
|
them into a nested object and coerces `"true"/"false"` → bool and numeric
|
|
487
487
|
strings → int. The normalized shape is
|
|
488
|
-
`{ schemaVersion, repository{provider,url,ref,depth,submodules,authRef}, branch{base,create,push}, setup{commands,env,secretRefs}, task{prompt,promptFile,maxIterations,timeoutMs,allowPr,prBase} }`.
|
|
488
|
+
`{ schemaVersion, repository{provider,url,ref,sha,depth,singleBranch,filter,baseRef,baseSha,cloneTimeoutMs,submodules,authRef}, branch{base,create,push}, setup{commands,env,secretRefs}, task{prompt,promptFile,maxIterations,timeoutMs,allowPr,prBase} }`.
|
|
489
489
|
|
|
490
490
|
**Prompt = base + optional verbatim append.** The agent's prompt resolves to
|
|
491
491
|
`task.prompt` (typically a model header filled at deploy time), falling back to a
|
|
@@ -531,8 +531,33 @@ the harness:
|
|
|
531
531
|
1. resolve the optional repo credential (`repository.authRef`, or `GITHUB_TOKEN`
|
|
532
532
|
for GitHub) — absent ⇒ anonymous clone;
|
|
533
533
|
2. `git clone` (honouring `depth`/`submodules`, and `repository.ref`/`branch.base`
|
|
534
|
-
as the checkout target)
|
|
535
|
-
|
|
534
|
+
as the checkout target). **`ref` is always a branch/tag name** (there is no hex
|
|
535
|
+
heuristic, so a legitimately hex-named branch like `deadbeef` is cloned via
|
|
536
|
+
`--branch`, never misread as a commit); to pin a **raw commit** use the
|
|
537
|
+
dedicated **`repository.sha`** field, which clones `ref`/`branch.base` (if any)
|
|
538
|
+
then fetches + checks the commit out as a detached HEAD. The clone lands in a
|
|
539
|
+
throwaway workspace under `<state>/agent-runs/run-*`. For a **huge monorepo**
|
|
540
|
+
the clone envelope can be
|
|
541
|
+
scoped so it finishes inside the clone timeout: **`singleBranch`** adds
|
|
542
|
+
`--single-branch` (fetch only `ref`, not every branch — a plain `clone --branch`
|
|
543
|
+
still pulls all branches/history); **`filter`** (e.g. `"blob:none"`) adds
|
|
544
|
+
`--filter=<spec>` for a partial/treeless clone (full commit graph, lazy blobs —
|
|
545
|
+
so `merge-base`/`git diff base...head` still work); **`baseRef`**/**`baseSha`**
|
|
546
|
+
additionally `git fetch` the base (respecting `depth`/`filter`) so a
|
|
547
|
+
single-branch/shallow clone can still diff `base...head` (exported as
|
|
548
|
+
`AGENT_REPO_BASE`). **`baseRef`** is always treated as a branch/tag name (even
|
|
549
|
+
one that looks hex-like) and is mapped into `refs/remotes/origin/<baseRef>`;
|
|
550
|
+
**`baseSha`** is the field for a raw commit SHA (fetched by id, exposed as the
|
|
551
|
+
SHA itself). `baseRef` and `baseSha` are **mutually exclusive** — setting both
|
|
552
|
+
is ambiguous, so the base fetch is skipped and a non-fatal `baseFetchError`
|
|
553
|
+
records the misconfiguration. A `--depth 1 --single-branch` of only the head
|
|
554
|
+
otherwise has NO
|
|
555
|
+
base and NO merge-base, so a naive `git diff main` fails. A failed base fetch
|
|
556
|
+
is **non-fatal** — the head clone still succeeds and
|
|
557
|
+
the failure is logged. **`cloneTimeoutMs`** overrides the clone/fetch timeout
|
|
558
|
+
per envelope (default 120s, or the `--clone-timeout` worker flag) as a backstop
|
|
559
|
+
for repos big enough to approach the cap even when shallow; a timeout is now
|
|
560
|
+
reported *as a timeout* rather than an opaque `exit 128`;
|
|
536
561
|
3. create `branch.create` (if set) off that target;
|
|
537
562
|
4. set a **committer identity** on the workspace, preferring the operator's own
|
|
538
563
|
(`GIT_AUTHOR_*` env → global `git config user.name/email` → the
|
|
@@ -540,8 +565,8 @@ the harness:
|
|
|
540
565
|
none resolve — so autonomous commits are authored by the human running the
|
|
541
566
|
fleet (who has signed any CLA/DCO), not an anonymous bot;
|
|
542
567
|
5. run the harness **in the workspace** (`cwd`), with `AGENT_WORKSPACE`,
|
|
543
|
-
`AGENT_REPO_URL`, `AGENT_REPO_BRANCH`, `AGENT_REPO_REF`
|
|
544
|
-
envelope on stdin;
|
|
568
|
+
`AGENT_REPO_URL`, `AGENT_REPO_BRANCH`, `AGENT_REPO_REF`, `AGENT_REPO_BASE`
|
|
569
|
+
exported and the job envelope on stdin;
|
|
545
570
|
6. on success, enumerate new commits, `git push` the branch when `branch.push`
|
|
546
571
|
(default true), and — when `task.allowPr` — **reconcile the PR the agent
|
|
547
572
|
opened** for the branch (`gh pr list --head <branch>`; `openedBy` reports the
|
package/c8ctl-plugin.js
CHANGED
|
@@ -2294,7 +2294,25 @@ function normalizeTaskEnvelope(customHeaders, variables, opts = {}) {
|
|
|
2294
2294
|
provider: (str(repo.provider) || 'github').toLowerCase(),
|
|
2295
2295
|
url: str(repo.url),
|
|
2296
2296
|
ref: str(repo.ref),
|
|
2297
|
+
// Dedicated field for a raw commit SHA to check out (detached), mirroring
|
|
2298
|
+
// baseSha/baseRef. `ref` is ALWAYS a branch/tag name — there is no hex
|
|
2299
|
+
// heuristic — so a legitimately hex-named branch (e.g. `deadbeef`) is
|
|
2300
|
+
// never misread as a commit; pin a commit via `sha` instead.
|
|
2301
|
+
sha: str(repo.sha),
|
|
2297
2302
|
depth: coerceInt(repo.depth, undefined),
|
|
2303
|
+
// Scope the fetch to just `ref` (independent of depth) for callers that want
|
|
2304
|
+
// full history of one branch but not every branch of a huge monorepo.
|
|
2305
|
+
singleBranch: coerceBool(repo.singleBranch, false),
|
|
2306
|
+
// Partial/treeless clone spec (e.g. "blob:none"): full commit graph with
|
|
2307
|
+
// lazy blob fetch, so `merge-base` / `git diff base...head` still work.
|
|
2308
|
+
filter: str(repo.filter),
|
|
2309
|
+
// When a base branch/sha is supplied we additionally fetch it so a
|
|
2310
|
+
// single-branch/shallow clone can still diff `base...head`.
|
|
2311
|
+
baseRef: str(repo.baseRef),
|
|
2312
|
+
baseSha: str(repo.baseSha),
|
|
2313
|
+
// Per-envelope override of the clone/fetch timeout (ms) — a backstop for
|
|
2314
|
+
// repos big enough to approach the default 120s cap even when shallow.
|
|
2315
|
+
cloneTimeoutMs: coerceInt(repo.cloneTimeoutMs, undefined),
|
|
2298
2316
|
submodules: coerceBool(repo.submodules, false),
|
|
2299
2317
|
authRef: str(repo.authRef),
|
|
2300
2318
|
};
|
|
@@ -2390,11 +2408,16 @@ function sameOrigin(a, b) {
|
|
|
2390
2408
|
}
|
|
2391
2409
|
}
|
|
2392
2410
|
|
|
2393
|
-
function resolveBrokerRestConfig(env = process.env) {
|
|
2411
|
+
function resolveBrokerRestConfig(env = process.env, opts = {}) {
|
|
2394
2412
|
// readConfig() swallows parse/IO errors and never throws (returns {}), so no
|
|
2395
2413
|
// local try/catch is needed here.
|
|
2396
2414
|
const cfg = readConfig() || {};
|
|
2415
|
+
// `opts.baseUrl` lets a caller pin the effective base (e.g. the active c8ctl
|
|
2416
|
+
// profile's REST address — see resolveAutoRestConfig) while still running it
|
|
2417
|
+
// through the SAME token same-origin gate below, so the token logic stays
|
|
2418
|
+
// single-sourced (never duplicated per call site).
|
|
2397
2419
|
const baseUrl =
|
|
2420
|
+
opts.baseUrl ||
|
|
2398
2421
|
env.NANO_REST_URL ||
|
|
2399
2422
|
env.NANO_BASE_URL ||
|
|
2400
2423
|
cfg.nanoUrl ||
|
|
@@ -2422,6 +2445,33 @@ function resolveBrokerRestConfig(env = process.env) {
|
|
|
2422
2445
|
return { baseUrl, token };
|
|
2423
2446
|
}
|
|
2424
2447
|
|
|
2448
|
+
// Resolve the C8 REST config the `--auto` engine-read reader is built from.
|
|
2449
|
+
// This is the job-type-read analogue of resolveLinkedPromptSource: an explicit
|
|
2450
|
+
// NANO_REST_URL / NANO_BASE_URL / cfg.nanoUrl override still wins (operator
|
|
2451
|
+
// escape hatch), but with NONE of those set the base is derived from the SAME
|
|
2452
|
+
// c8ctl client that activates jobs (its getConfig().restAddress) rather than the
|
|
2453
|
+
// localhost default — so a worker that can activate jobs against a profile
|
|
2454
|
+
// engine can also read the deployed job types from it. Without this, an `--auto`
|
|
2455
|
+
// worker on an active remote profile reads from http://localhost:8080, finds no
|
|
2456
|
+
// engine, discovers 0 job types, and crash-loops (jwulf/c8ctl-plugin-nano#93).
|
|
2457
|
+
// Falls back to resolveBrokerRestConfig's localhost default only when the client
|
|
2458
|
+
// exposes no usable restAddress. The token same-origin gate lives in
|
|
2459
|
+
// resolveBrokerRestConfig (re-run against the profile base), never duplicated.
|
|
2460
|
+
function resolveAutoRestConfig(camunda, env = process.env) {
|
|
2461
|
+
const cfg = readConfig() || {};
|
|
2462
|
+
const hasExplicitBase = Boolean(env.NANO_REST_URL || env.NANO_BASE_URL || cfg.nanoUrl);
|
|
2463
|
+
if (!hasExplicitBase && camunda && typeof camunda.getConfig === 'function') {
|
|
2464
|
+
let profileBase = '';
|
|
2465
|
+
try {
|
|
2466
|
+
profileBase = normalizeRestBase(camunda.getConfig()?.restAddress);
|
|
2467
|
+
} catch {
|
|
2468
|
+
// ignore — degrade to the resolveBrokerRestConfig (localhost) default below
|
|
2469
|
+
}
|
|
2470
|
+
if (profileBase) return resolveBrokerRestConfig(env, { baseUrl: profileBase });
|
|
2471
|
+
}
|
|
2472
|
+
return resolveBrokerRestConfig(env);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2425
2475
|
// ---------------------------------------------------------------------------
|
|
2426
2476
|
// `nano work --auto`: zero-config engine-read enrolment (issue #66).
|
|
2427
2477
|
//
|
|
@@ -2790,12 +2840,51 @@ function redactToken(text, token) {
|
|
|
2790
2840
|
return s.replace(/(https?:\/\/)[^@/\s]+@/gi, '$1');
|
|
2791
2841
|
}
|
|
2792
2842
|
|
|
2843
|
+
// Build an operator-actionable diagnostic from one or more runGit results.
|
|
2844
|
+
// git splits its output unpredictably across stdout/stderr, so preferring one
|
|
2845
|
+
// stream (`stderr || stdout`) can drop the only useful line — the root of the
|
|
2846
|
+
// "stub reason"/"opaque exit 128" incidents. Combine BOTH streams of every
|
|
2847
|
+
// command, redact the token, and always append status/signal context (from the
|
|
2848
|
+
// last, i.e. failing, command) so an empty-output failure still says something.
|
|
2849
|
+
function gitErrorDetail(results, token, limit = 500) {
|
|
2850
|
+
const list = Array.isArray(results) ? results : [results];
|
|
2851
|
+
const body = redactToken(
|
|
2852
|
+
list
|
|
2853
|
+
.flatMap((r) => [r?.stderr, r?.stdout])
|
|
2854
|
+
.map((s) => String(s ?? '').trim())
|
|
2855
|
+
.filter(Boolean)
|
|
2856
|
+
.join('\n'),
|
|
2857
|
+
token,
|
|
2858
|
+
).trim().slice(0, limit);
|
|
2859
|
+
const last = list[list.length - 1] || {};
|
|
2860
|
+
const ctx = [];
|
|
2861
|
+
if (last.status != null) ctx.push(`exit ${last.status}`);
|
|
2862
|
+
if (last.signal) ctx.push(`signal ${last.signal}`);
|
|
2863
|
+
const ctxStr = ctx.length ? `(${ctx.join(', ')})` : '';
|
|
2864
|
+
return [body, ctxStr].filter(Boolean).join(' ') || 'unknown error';
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2793
2867
|
function runGit(args, { cwd, env, timeoutMs = 120_000 } = {}) {
|
|
2794
2868
|
try {
|
|
2795
2869
|
const r = spawnSync('git', args, { cwd, env, encoding: 'utf8', timeout: timeoutMs });
|
|
2796
|
-
|
|
2870
|
+
// spawnSync does not throw on timeout — it returns with `error.code` set to
|
|
2871
|
+
// 'ETIMEDOUT' and the child SIGTERM-killed (signal set, status null). Surface
|
|
2872
|
+
// that as `timedOut` so callers can report a timeout instead of an
|
|
2873
|
+
// uninformative "exit 128" (ties to #89).
|
|
2874
|
+
const timedOut = !!(r.error && r.error.code === 'ETIMEDOUT');
|
|
2875
|
+
// A non-timeout spawn failure (e.g. ENOENT when `git` is missing, EACCES)
|
|
2876
|
+
// comes back via `r.error` with empty stdout/stderr; discarding
|
|
2877
|
+
// `r.error.message` leaves callers reporting an empty/"unknown error"
|
|
2878
|
+
// detail. Fold the spawn error (code + message) into stderr so
|
|
2879
|
+
// `gitErrorDetail` still surfaces something actionable.
|
|
2880
|
+
let stderr = r.stderr || '';
|
|
2881
|
+
if (r.error && !timedOut) {
|
|
2882
|
+
const spawnMsg = [r.error.code, r.error.message].filter(Boolean).join(': ');
|
|
2883
|
+
stderr = [stderr.trim(), spawnMsg].filter(Boolean).join('\n');
|
|
2884
|
+
}
|
|
2885
|
+
return { status: r.status ?? (r.signal ? 128 : null), stdout: r.stdout || '', stderr, signal: r.signal || null, timedOut, timeoutMs };
|
|
2797
2886
|
} catch (err) {
|
|
2798
|
-
return { status: null, stdout: '', stderr: err.message, signal: null };
|
|
2887
|
+
return { status: null, stdout: '', stderr: err.message, signal: null, timedOut: false, timeoutMs };
|
|
2799
2888
|
}
|
|
2800
2889
|
}
|
|
2801
2890
|
|
|
@@ -3115,38 +3204,108 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
|
|
|
3115
3204
|
gitEnv.GIT_CONFIG_GLOBAL = devNull;
|
|
3116
3205
|
}
|
|
3117
3206
|
|
|
3118
|
-
const
|
|
3119
|
-
//
|
|
3120
|
-
//
|
|
3121
|
-
|
|
3207
|
+
const branchName = repo.ref || envelope.branch?.base || '';
|
|
3208
|
+
// A raw commit is requested ONLY via the dedicated `repository.sha` field —
|
|
3209
|
+
// the sole unambiguous way to pin a commit. `ref` (→ `branchName`) is ALWAYS a
|
|
3210
|
+
// branch/tag name and is passed to `git clone --branch`; there is no hex
|
|
3211
|
+
// heuristic, so a legitimately hex-named branch like `deadbeef` is cloned as a
|
|
3212
|
+
// branch, not misread as a SHA. When a `sha` is given we clone `branchName`
|
|
3213
|
+
// (if any — the branch that should contain it) then fetch + detach onto it.
|
|
3214
|
+
const commitSha = repo.sha || '';
|
|
3215
|
+
// `sha` pins a raw commit and is passed to `git fetch origin <sha>` /
|
|
3216
|
+
// `git checkout --detach <sha>`. Validate it is a hex commit id (7–40 chars)
|
|
3217
|
+
// before use: this fails a misconfigured envelope fast and, because a hex id
|
|
3218
|
+
// can never start with `-`, forecloses a value being (mis)parsed as a git
|
|
3219
|
+
// option.
|
|
3220
|
+
if (commitSha && !/^[0-9a-f]{7,40}$/i.test(commitSha)) {
|
|
3221
|
+
throw new ProvisionError(`invalid repository.sha ${JSON.stringify(commitSha)} — expected a hex commit id (7–40 chars)`);
|
|
3222
|
+
}
|
|
3223
|
+
const isSha = !!commitSha;
|
|
3224
|
+
// Per-envelope timeout override (backstop for giant monorepos that approach the
|
|
3225
|
+
// default cap even when shallow); falls back to the caller-supplied timeout.
|
|
3226
|
+
const effectiveTimeoutMs = (repo.cloneTimeoutMs && repo.cloneTimeoutMs > 0) ? repo.cloneTimeoutMs : timeoutMs;
|
|
3122
3227
|
const cloneArgs = [...credArgs(), 'clone', '--no-tags'];
|
|
3123
3228
|
if (repo.depth && repo.depth > 0) cloneArgs.push('--depth', String(repo.depth));
|
|
3229
|
+
// `--single-branch` restricts the fetch to just `ref` — a plain `clone --branch`
|
|
3230
|
+
// still downloads every branch and all history. `--depth` implies this, but
|
|
3231
|
+
// honor it independently for a full-history single-branch clone.
|
|
3232
|
+
if (repo.singleBranch) cloneArgs.push('--single-branch');
|
|
3233
|
+
// Partial (blob-filtered) clone: full commit graph, lazy blobs — best fit for
|
|
3234
|
+
// reviewing a PR on a monorepo where a full checkout blows the timeout.
|
|
3235
|
+
if (repo.filter) cloneArgs.push(`--filter=${repo.filter}`);
|
|
3124
3236
|
if (repo.submodules) cloneArgs.push('--recurse-submodules');
|
|
3125
|
-
if (
|
|
3237
|
+
if (branchName) cloneArgs.push('--branch', branchName);
|
|
3126
3238
|
const remote = authUrl(repo.url, repo.provider || 'github', !!token);
|
|
3127
3239
|
cloneArgs.push(remote, workspaceDir);
|
|
3128
3240
|
|
|
3129
|
-
const clone = runGit(cloneArgs, { env: gitEnv, timeoutMs });
|
|
3241
|
+
const clone = runGit(cloneArgs, { env: gitEnv, timeoutMs: effectiveTimeoutMs });
|
|
3130
3242
|
if (clone.status !== 0) {
|
|
3131
|
-
|
|
3243
|
+
if (clone.timedOut) {
|
|
3244
|
+
// Preserve whatever git managed to print before SIGTERM (plus exit/signal
|
|
3245
|
+
// context) so a timeout is still diagnosable, not an opaque wall-clock hit.
|
|
3246
|
+
const detail = gitErrorDetail(clone, token);
|
|
3247
|
+
const detailNote = detail && detail !== 'unknown error' ? ` — last git output: ${detail}` : '';
|
|
3248
|
+
throw new ProvisionError(`git clone timed out after ${effectiveTimeoutMs}ms — the repo may be too large, or the network stalled; raise the timeout (repository.cloneTimeoutMs in the envelope, or the worker's --clone-timeout flag) or scope the clone with filter/singleBranch/depth${detailNote}`);
|
|
3249
|
+
}
|
|
3250
|
+
throw new ProvisionError(`git clone failed: ${gitErrorDetail(clone, token)}`);
|
|
3132
3251
|
}
|
|
3133
3252
|
|
|
3134
3253
|
if (isSha) {
|
|
3135
|
-
// The SHA may not be present under a shallow clone of the
|
|
3136
|
-
//
|
|
3137
|
-
const fetch = runGit([...credArgs(), 'fetch', '--no-tags', 'origin',
|
|
3138
|
-
const co = runGit(['checkout', '--detach',
|
|
3254
|
+
// The SHA may not be present under a shallow clone of the branch — fetch it
|
|
3255
|
+
// explicitly (best effort), then check it out (detached HEAD).
|
|
3256
|
+
const fetch = runGit([...credArgs(), 'fetch', '--no-tags', 'origin', commitSha], { cwd: workspaceDir, env: gitEnv, timeoutMs: effectiveTimeoutMs });
|
|
3257
|
+
const co = runGit(['checkout', '--detach', commitSha], { cwd: workspaceDir, env: gitEnv });
|
|
3139
3258
|
if (co.status !== 0) {
|
|
3140
|
-
//
|
|
3141
|
-
//
|
|
3142
|
-
//
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3259
|
+
// Combine the fetch + checkout output (the real reason often lives in the
|
|
3260
|
+
// fetch), and annotate a fetch timeout explicitly so a slow `git fetch
|
|
3261
|
+
// origin <sha>` is not misread as an opaque checkout failure.
|
|
3262
|
+
const fetchNote = fetch.timedOut ? ` (preceding git fetch origin ${commitSha} timed out after ${effectiveTimeoutMs}ms)` : '';
|
|
3263
|
+
throw new ProvisionError(`git checkout ${commitSha} failed: ${gitErrorDetail([fetch, co], token, 300)}${fetchNote}`);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
// Optional base fetch: with a single-branch/shallow clone the head has no base
|
|
3268
|
+
// and no merge-base, so a naive `git diff <base>` fails. When a base branch or
|
|
3269
|
+
// sha is supplied, fetch it (respecting depth/filter) into a remote-tracking
|
|
3270
|
+
// ref so the harness can compute `git diff origin/<base>...HEAD`. Best-effort:
|
|
3271
|
+
// a failed base fetch is recorded, not fatal (the head clone still succeeded).
|
|
3272
|
+
let base = '';
|
|
3273
|
+
let baseFetchError;
|
|
3274
|
+
// `baseRef` (branch/tag) and `baseSha` (raw commit) are mutually exclusive — a
|
|
3275
|
+
// caller picks one. If BOTH are set the envelope is ambiguous, so rather than
|
|
3276
|
+
// silently preferring one (a surprising `base...head` diff), skip the base
|
|
3277
|
+
// fetch and record a non-fatal diagnostic so the misconfiguration is visible.
|
|
3278
|
+
if (repo.baseSha && repo.baseRef) {
|
|
3279
|
+
baseFetchError = `ambiguous base: both baseRef (${repo.baseRef}) and baseSha (${repo.baseSha}) set — provide only one`;
|
|
3280
|
+
} else {
|
|
3281
|
+
const baseTarget = repo.baseSha || repo.baseRef;
|
|
3282
|
+
if (baseTarget) {
|
|
3283
|
+
const isBaseSha = !!repo.baseSha;
|
|
3284
|
+
const fetchArgs = [...credArgs(), 'fetch', '--no-tags'];
|
|
3285
|
+
if (repo.depth && repo.depth > 0) fetchArgs.push('--depth', String(repo.depth));
|
|
3286
|
+
if (repo.filter) fetchArgs.push(`--filter=${repo.filter}`);
|
|
3287
|
+
if (isBaseSha) {
|
|
3288
|
+
// A raw sha can't be mapped to a stable name — fetch it (updates FETCH_HEAD)
|
|
3289
|
+
// and expose the sha itself as the diff base.
|
|
3290
|
+
fetchArgs.push('origin', baseTarget);
|
|
3291
|
+
base = baseTarget;
|
|
3292
|
+
} else {
|
|
3293
|
+
// Map the branch onto refs/remotes/origin/<baseRef> so `origin/<base>`
|
|
3294
|
+
// resolves for the reviewer even on a single-branch clone.
|
|
3295
|
+
fetchArgs.push('origin', `+${baseTarget}:refs/remotes/origin/${baseTarget}`);
|
|
3296
|
+
base = `origin/${baseTarget}`;
|
|
3297
|
+
}
|
|
3298
|
+
const bf = runGit(fetchArgs, { cwd: workspaceDir, env: gitEnv, timeoutMs: effectiveTimeoutMs });
|
|
3299
|
+
if (bf.status !== 0) {
|
|
3300
|
+
// Name the base target (branch vs sha) so the warning is actionable when
|
|
3301
|
+
// multiple refs are in play, and preserve git's output/context in both the
|
|
3302
|
+
// timeout and non-timeout paths.
|
|
3303
|
+
const baseLabel = `${isBaseSha ? 'sha' : 'branch'} ${baseTarget}`;
|
|
3304
|
+
base = '';
|
|
3305
|
+
baseFetchError = bf.timedOut
|
|
3306
|
+
? `base fetch (${baseLabel}) timed out after ${effectiveTimeoutMs}ms: ${gitErrorDetail(bf, token, 300)}`
|
|
3307
|
+
: `base fetch (${baseLabel}) failed: ${gitErrorDetail(bf, token, 300)}`;
|
|
3308
|
+
}
|
|
3150
3309
|
}
|
|
3151
3310
|
}
|
|
3152
3311
|
|
|
@@ -3193,7 +3352,7 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
|
|
|
3193
3352
|
// `git rev-parse HEAD` on an unborn branch (freshly cloned empty repo) exits
|
|
3194
3353
|
// non-zero and echoes the literal "HEAD" on stdout — treat that as "no base
|
|
3195
3354
|
// commit" (empty startSha) rather than a bogus revision.
|
|
3196
|
-
return { workspaceDir, gitEnv, committer, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref:
|
|
3355
|
+
return { workspaceDir, gitEnv, committer, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref: commitSha || branchName || '', base, baseFetchError, remote: redactToken(repo.url, token) };
|
|
3197
3356
|
}
|
|
3198
3357
|
|
|
3199
3358
|
// Look up a PR for this branch (2a does NOT open it — the harness does, driven
|
|
@@ -4431,10 +4590,14 @@ async function workAgent(req, flags) {
|
|
|
4431
4590
|
}
|
|
4432
4591
|
const camunda = globalThis.c8ctl.createClient();
|
|
4433
4592
|
|
|
4434
|
-
// Broker REST endpoint for live linked-resource prompts (issue #63)
|
|
4435
|
-
//
|
|
4436
|
-
//
|
|
4437
|
-
|
|
4593
|
+
// Broker REST endpoint for live linked-resource prompts (issue #63) and the
|
|
4594
|
+
// C8 REST source for `--auto`'s engine-read enrolment. Derived from the SAME
|
|
4595
|
+
// client that activates jobs (its profile REST address) when no explicit
|
|
4596
|
+
// NANO_REST_URL/NANO_BASE_URL/cfg.nanoUrl override is set — so a worker that
|
|
4597
|
+
// can activate jobs against the active profile engine also reads job types
|
|
4598
|
+
// from it, instead of a localhost default that crash-loops when nothing is
|
|
4599
|
+
// listening on :8080 (jwulf/c8ctl-plugin-nano#93). Resolved once at startup.
|
|
4600
|
+
const restConfig = resolveAutoRestConfig(camunda);
|
|
4438
4601
|
|
|
4439
4602
|
// The desired job-type set. In `--auto` it is engine-read (∪ any --job-type
|
|
4440
4603
|
// extras); otherwise it is the rank×capability matrix (∪ extras). The initial
|
|
@@ -4725,12 +4888,19 @@ async function workAgent(req, flags) {
|
|
|
4725
4888
|
runDir = mkdtempSync(join(agentRunsRoot(), 'run-'));
|
|
4726
4889
|
liveRunDirs.add(runDir);
|
|
4727
4890
|
provisioned = provisionRepo({ envelope, token: repoToken, runDir, timeoutMs: cloneTimeoutMs });
|
|
4891
|
+
if (provisioned.baseFetchError) {
|
|
4892
|
+
logger.warn(`[${jobType}] job ${job.jobKey} base fetch failed — ${provisioned.baseFetchError}; base...head diffs may be unavailable`);
|
|
4893
|
+
}
|
|
4728
4894
|
cwd = provisioned.workspaceDir;
|
|
4729
4895
|
extraEnv = {
|
|
4730
4896
|
AGENT_WORKSPACE: provisioned.workspaceDir,
|
|
4731
4897
|
AGENT_REPO_URL: provisioned.remote,
|
|
4732
4898
|
AGENT_REPO_BRANCH: provisioned.workingBranch || '',
|
|
4733
4899
|
AGENT_REPO_REF: provisioned.ref || '',
|
|
4900
|
+
// The fetched base ref (e.g. `origin/main` or a base sha) for
|
|
4901
|
+
// computing `git diff <base>...HEAD`; empty when none was requested
|
|
4902
|
+
// or the base fetch failed (see provisioned.baseFetchError).
|
|
4903
|
+
AGENT_REPO_BASE: provisioned.base || '',
|
|
4734
4904
|
// Pin the harness's commit identity to the resolved (placeholder-
|
|
4735
4905
|
// sanitized) committer so the agent's own `git commit` can't be
|
|
4736
4906
|
// hijacked by a placeholder GIT_AUTHOR_* inherited from process.env
|
|
@@ -5049,7 +5219,16 @@ async function workAgent(req, flags) {
|
|
|
5049
5219
|
if (inFlightReconcile) return;
|
|
5050
5220
|
reconcile().catch((err) => logger.warn(`--auto reconcile failed: ${err?.message || err}`));
|
|
5051
5221
|
}, AUTO_POLL_INTERVAL_MS);
|
|
5052
|
-
|
|
5222
|
+
// Deliberately REF'd (unlike the reaper/run-dir hygiene timers, which are
|
|
5223
|
+
// unref'd): in `--auto` this poll IS the retry loop, and it must keep the
|
|
5224
|
+
// process alive even with zero pollers. When the INITIAL engine read fails
|
|
5225
|
+
// (transient miss, or the engine isn't up yet) the worker registers 0
|
|
5226
|
+
// pollers; nothing else holds the event loop open (the SDK client with no
|
|
5227
|
+
// job workers doesn't, and the hygiene timers are unref'd), so an unref'd
|
|
5228
|
+
// poll timer would let the process exit 0 — the observed crash-loop under a
|
|
5229
|
+
// supervisor (jwulf/c8ctl-plugin-nano#93). Keeping it ref'd makes the worker
|
|
5230
|
+
// stay up and re-read on the next poll, exactly as the initial-read warning
|
|
5231
|
+
// promises. Shutdown clears it (clearInterval), so Ctrl-C/SIGTERM still exit.
|
|
5053
5232
|
} else {
|
|
5054
5233
|
// `watchFile` (polling stat) is deliberate over `fs.watch`: it survives the
|
|
5055
5234
|
// atomic temp+rename that `writeConfig` does (fs.watch would rebind to the old
|
|
@@ -8413,6 +8592,7 @@ export {
|
|
|
8413
8592
|
parseLinkedResources,
|
|
8414
8593
|
pickLinkedResource,
|
|
8415
8594
|
resolveBrokerRestConfig,
|
|
8595
|
+
resolveAutoRestConfig,
|
|
8416
8596
|
resourceContentUrl,
|
|
8417
8597
|
fetchLinkedResourceContent,
|
|
8418
8598
|
resolveLinkedPrompt,
|
|
@@ -8468,6 +8648,7 @@ export {
|
|
|
8468
8648
|
scanAgentTaskLeaves,
|
|
8469
8649
|
readDeployedAgentJobTypes,
|
|
8470
8650
|
resolveAutoJobTypes,
|
|
8651
|
+
workAgent,
|
|
8471
8652
|
derivePollTimeoutMs,
|
|
8472
8653
|
AGENT_TASK_NS,
|
|
8473
8654
|
AGENT_RESULT_KEY,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.36.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
|
|
6
6
|
"main": "c8ctl-plugin.js",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
},
|
|
58
58
|
"optionalDependencies": {
|
|
59
59
|
"node-pty": "^1.0.0",
|
|
60
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.
|
|
61
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
62
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
63
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
64
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
65
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
66
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.
|
|
60
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.36.1",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.36.1",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.36.1",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.36.1",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.36.1",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.36.1",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.36.1"
|
|
67
67
|
}
|
|
68
68
|
}
|