c8ctl-plugin-nano 1.35.5 → 1.36.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/README.md +30 -5
- package/c8ctl-plugin.js +211 -20
- 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
|
};
|
|
@@ -2790,13 +2808,98 @@ function redactToken(text, token) {
|
|
|
2790
2808
|
return s.replace(/(https?:\/\/)[^@/\s]+@/gi, '$1');
|
|
2791
2809
|
}
|
|
2792
2810
|
|
|
2811
|
+
// Build an operator-actionable diagnostic from one or more runGit results.
|
|
2812
|
+
// git splits its output unpredictably across stdout/stderr, so preferring one
|
|
2813
|
+
// stream (`stderr || stdout`) can drop the only useful line — the root of the
|
|
2814
|
+
// "stub reason"/"opaque exit 128" incidents. Combine BOTH streams of every
|
|
2815
|
+
// command, redact the token, and always append status/signal context (from the
|
|
2816
|
+
// last, i.e. failing, command) so an empty-output failure still says something.
|
|
2817
|
+
function gitErrorDetail(results, token, limit = 500) {
|
|
2818
|
+
const list = Array.isArray(results) ? results : [results];
|
|
2819
|
+
const body = redactToken(
|
|
2820
|
+
list
|
|
2821
|
+
.flatMap((r) => [r?.stderr, r?.stdout])
|
|
2822
|
+
.map((s) => String(s ?? '').trim())
|
|
2823
|
+
.filter(Boolean)
|
|
2824
|
+
.join('\n'),
|
|
2825
|
+
token,
|
|
2826
|
+
).trim().slice(0, limit);
|
|
2827
|
+
const last = list[list.length - 1] || {};
|
|
2828
|
+
const ctx = [];
|
|
2829
|
+
if (last.status != null) ctx.push(`exit ${last.status}`);
|
|
2830
|
+
if (last.signal) ctx.push(`signal ${last.signal}`);
|
|
2831
|
+
const ctxStr = ctx.length ? `(${ctx.join(', ')})` : '';
|
|
2832
|
+
return [body, ctxStr].filter(Boolean).join(' ') || 'unknown error';
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2793
2835
|
function runGit(args, { cwd, env, timeoutMs = 120_000 } = {}) {
|
|
2794
2836
|
try {
|
|
2795
2837
|
const r = spawnSync('git', args, { cwd, env, encoding: 'utf8', timeout: timeoutMs });
|
|
2796
|
-
|
|
2838
|
+
// spawnSync does not throw on timeout — it returns with `error.code` set to
|
|
2839
|
+
// 'ETIMEDOUT' and the child SIGTERM-killed (signal set, status null). Surface
|
|
2840
|
+
// that as `timedOut` so callers can report a timeout instead of an
|
|
2841
|
+
// uninformative "exit 128" (ties to #89).
|
|
2842
|
+
const timedOut = !!(r.error && r.error.code === 'ETIMEDOUT');
|
|
2843
|
+
// A non-timeout spawn failure (e.g. ENOENT when `git` is missing, EACCES)
|
|
2844
|
+
// comes back via `r.error` with empty stdout/stderr; discarding
|
|
2845
|
+
// `r.error.message` leaves callers reporting an empty/"unknown error"
|
|
2846
|
+
// detail. Fold the spawn error (code + message) into stderr so
|
|
2847
|
+
// `gitErrorDetail` still surfaces something actionable.
|
|
2848
|
+
let stderr = r.stderr || '';
|
|
2849
|
+
if (r.error && !timedOut) {
|
|
2850
|
+
const spawnMsg = [r.error.code, r.error.message].filter(Boolean).join(': ');
|
|
2851
|
+
stderr = [stderr.trim(), spawnMsg].filter(Boolean).join('\n');
|
|
2852
|
+
}
|
|
2853
|
+
return { status: r.status ?? (r.signal ? 128 : null), stdout: r.stdout || '', stderr, signal: r.signal || null, timedOut, timeoutMs };
|
|
2797
2854
|
} catch (err) {
|
|
2798
|
-
return { status: null, stdout: '', stderr: err.message, signal: null };
|
|
2855
|
+
return { status: null, stdout: '', stderr: err.message, signal: null, timedOut: false, timeoutMs };
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
// Bound a git output string without decapitating it: a hard `.slice(0, max)`
|
|
2860
|
+
// drops the tail, but the real reason can live at either end of a long
|
|
2861
|
+
// multiline git error (the `fatal:` up top, wrapping/hint detail below). Keep a
|
|
2862
|
+
// head+tail window joined by an elision marker so both survive.
|
|
2863
|
+
function boundGitOutput(text, max = 500) {
|
|
2864
|
+
const s = String(text ?? '').trim();
|
|
2865
|
+
if (s.length <= max) return s;
|
|
2866
|
+
const marker = ' […] ';
|
|
2867
|
+
// When max is too small to fit even the elision marker the head+tail budget
|
|
2868
|
+
// goes negative, making the slices behave unexpectedly and return MORE than
|
|
2869
|
+
// max — so hard-cap to max (never below zero) in that degenerate case.
|
|
2870
|
+
if (max <= marker.length) return s.slice(0, Math.max(0, max));
|
|
2871
|
+
const budget = max - marker.length;
|
|
2872
|
+
const head = Math.ceil(budget * 0.6);
|
|
2873
|
+
const tail = budget - head;
|
|
2874
|
+
return `${s.slice(0, head)}${marker}${s.slice(s.length - tail)}`;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
// Build an informative, token-redacted failure reason from a runGit result.
|
|
2878
|
+
// Two things the old `stderr || stdout`.slice(0,500) message threw away:
|
|
2879
|
+
// 1. On a timeout Node SIGTERM-kills git (status→128, signal='SIGTERM') — say
|
|
2880
|
+
// so, and after how long, instead of surfacing only git's flushed
|
|
2881
|
+
// "Cloning into '…'..." progress line as if it were the failure.
|
|
2882
|
+
// 2. `stderr || stdout` let a non-empty-but-useless stderr mask a real reason
|
|
2883
|
+
// on stdout — capture BOTH streams (stderr then stdout) so either survives.
|
|
2884
|
+
function describeGitFailure(action, result, { token, timeoutMs } = {}) {
|
|
2885
|
+
const combined = boundGitOutput(
|
|
2886
|
+
redactToken([result && result.stderr, result && result.stdout].filter(Boolean).join('\n'), token),
|
|
2887
|
+
);
|
|
2888
|
+
// spawnSync's timeout kill lands as SIGTERM (its default killSignal); runGit
|
|
2889
|
+
// maps the null status to 128. Only SIGTERM means "timed out" — any OTHER
|
|
2890
|
+
// signal (e.g. SIGKILL from an OOM kill) is a distinct termination that we
|
|
2891
|
+
// must not misreport as a timeout.
|
|
2892
|
+
const signal = result && result.signal;
|
|
2893
|
+
if (signal === 'SIGTERM') {
|
|
2894
|
+
const secs = timeoutMs ? Math.round(timeoutMs / 1000) : null;
|
|
2895
|
+
const dur = secs ? ` after ${secs}s` : '';
|
|
2896
|
+
return `${action} timed out${dur} (SIGTERM)${combined ? `; last output: ${combined}` : ''}`;
|
|
2799
2897
|
}
|
|
2898
|
+
if (signal) {
|
|
2899
|
+
return `${action} terminated by signal ${signal}${combined ? `: ${combined}` : ''}`;
|
|
2900
|
+
}
|
|
2901
|
+
const exit = result && result.status != null ? result.status : '?';
|
|
2902
|
+
return `${action} failed (exit ${exit})${combined ? `: ${combined}` : ''}`;
|
|
2800
2903
|
}
|
|
2801
2904
|
|
|
2802
2905
|
// Write a GIT_ASKPASS helper that echoes $GIT_TOKEN, so the token reaches git
|
|
@@ -3069,30 +3172,108 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
|
|
|
3069
3172
|
gitEnv.GIT_CONFIG_GLOBAL = devNull;
|
|
3070
3173
|
}
|
|
3071
3174
|
|
|
3072
|
-
const
|
|
3073
|
-
//
|
|
3074
|
-
//
|
|
3075
|
-
|
|
3175
|
+
const branchName = repo.ref || envelope.branch?.base || '';
|
|
3176
|
+
// A raw commit is requested ONLY via the dedicated `repository.sha` field —
|
|
3177
|
+
// the sole unambiguous way to pin a commit. `ref` (→ `branchName`) is ALWAYS a
|
|
3178
|
+
// branch/tag name and is passed to `git clone --branch`; there is no hex
|
|
3179
|
+
// heuristic, so a legitimately hex-named branch like `deadbeef` is cloned as a
|
|
3180
|
+
// branch, not misread as a SHA. When a `sha` is given we clone `branchName`
|
|
3181
|
+
// (if any — the branch that should contain it) then fetch + detach onto it.
|
|
3182
|
+
const commitSha = repo.sha || '';
|
|
3183
|
+
// `sha` pins a raw commit and is passed to `git fetch origin <sha>` /
|
|
3184
|
+
// `git checkout --detach <sha>`. Validate it is a hex commit id (7–40 chars)
|
|
3185
|
+
// before use: this fails a misconfigured envelope fast and, because a hex id
|
|
3186
|
+
// can never start with `-`, forecloses a value being (mis)parsed as a git
|
|
3187
|
+
// option.
|
|
3188
|
+
if (commitSha && !/^[0-9a-f]{7,40}$/i.test(commitSha)) {
|
|
3189
|
+
throw new ProvisionError(`invalid repository.sha ${JSON.stringify(commitSha)} — expected a hex commit id (7–40 chars)`);
|
|
3190
|
+
}
|
|
3191
|
+
const isSha = !!commitSha;
|
|
3192
|
+
// Per-envelope timeout override (backstop for giant monorepos that approach the
|
|
3193
|
+
// default cap even when shallow); falls back to the caller-supplied timeout.
|
|
3194
|
+
const effectiveTimeoutMs = (repo.cloneTimeoutMs && repo.cloneTimeoutMs > 0) ? repo.cloneTimeoutMs : timeoutMs;
|
|
3076
3195
|
const cloneArgs = [...credArgs(), 'clone', '--no-tags'];
|
|
3077
3196
|
if (repo.depth && repo.depth > 0) cloneArgs.push('--depth', String(repo.depth));
|
|
3197
|
+
// `--single-branch` restricts the fetch to just `ref` — a plain `clone --branch`
|
|
3198
|
+
// still downloads every branch and all history. `--depth` implies this, but
|
|
3199
|
+
// honor it independently for a full-history single-branch clone.
|
|
3200
|
+
if (repo.singleBranch) cloneArgs.push('--single-branch');
|
|
3201
|
+
// Partial (blob-filtered) clone: full commit graph, lazy blobs — best fit for
|
|
3202
|
+
// reviewing a PR on a monorepo where a full checkout blows the timeout.
|
|
3203
|
+
if (repo.filter) cloneArgs.push(`--filter=${repo.filter}`);
|
|
3078
3204
|
if (repo.submodules) cloneArgs.push('--recurse-submodules');
|
|
3079
|
-
if (
|
|
3205
|
+
if (branchName) cloneArgs.push('--branch', branchName);
|
|
3080
3206
|
const remote = authUrl(repo.url, repo.provider || 'github', !!token);
|
|
3081
3207
|
cloneArgs.push(remote, workspaceDir);
|
|
3082
3208
|
|
|
3083
|
-
const clone = runGit(cloneArgs, { env: gitEnv, timeoutMs });
|
|
3209
|
+
const clone = runGit(cloneArgs, { env: gitEnv, timeoutMs: effectiveTimeoutMs });
|
|
3084
3210
|
if (clone.status !== 0) {
|
|
3085
|
-
|
|
3211
|
+
if (clone.timedOut) {
|
|
3212
|
+
// Preserve whatever git managed to print before SIGTERM (plus exit/signal
|
|
3213
|
+
// context) so a timeout is still diagnosable, not an opaque wall-clock hit.
|
|
3214
|
+
const detail = gitErrorDetail(clone, token);
|
|
3215
|
+
const detailNote = detail && detail !== 'unknown error' ? ` — last git output: ${detail}` : '';
|
|
3216
|
+
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}`);
|
|
3217
|
+
}
|
|
3218
|
+
throw new ProvisionError(`git clone failed: ${gitErrorDetail(clone, token)}`);
|
|
3086
3219
|
}
|
|
3087
3220
|
|
|
3088
3221
|
if (isSha) {
|
|
3089
|
-
// The SHA may not be present under a shallow clone of the
|
|
3090
|
-
//
|
|
3091
|
-
const fetch = runGit([...credArgs(), 'fetch', '--no-tags', 'origin',
|
|
3092
|
-
const co = runGit(['checkout', '--detach',
|
|
3222
|
+
// The SHA may not be present under a shallow clone of the branch — fetch it
|
|
3223
|
+
// explicitly (best effort), then check it out (detached HEAD).
|
|
3224
|
+
const fetch = runGit([...credArgs(), 'fetch', '--no-tags', 'origin', commitSha], { cwd: workspaceDir, env: gitEnv, timeoutMs: effectiveTimeoutMs });
|
|
3225
|
+
const co = runGit(['checkout', '--detach', commitSha], { cwd: workspaceDir, env: gitEnv });
|
|
3093
3226
|
if (co.status !== 0) {
|
|
3094
|
-
|
|
3095
|
-
|
|
3227
|
+
// Combine the fetch + checkout output (the real reason often lives in the
|
|
3228
|
+
// fetch), and annotate a fetch timeout explicitly so a slow `git fetch
|
|
3229
|
+
// origin <sha>` is not misread as an opaque checkout failure.
|
|
3230
|
+
const fetchNote = fetch.timedOut ? ` (preceding git fetch origin ${commitSha} timed out after ${effectiveTimeoutMs}ms)` : '';
|
|
3231
|
+
throw new ProvisionError(`git checkout ${commitSha} failed: ${gitErrorDetail([fetch, co], token, 300)}${fetchNote}`);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
// Optional base fetch: with a single-branch/shallow clone the head has no base
|
|
3236
|
+
// and no merge-base, so a naive `git diff <base>` fails. When a base branch or
|
|
3237
|
+
// sha is supplied, fetch it (respecting depth/filter) into a remote-tracking
|
|
3238
|
+
// ref so the harness can compute `git diff origin/<base>...HEAD`. Best-effort:
|
|
3239
|
+
// a failed base fetch is recorded, not fatal (the head clone still succeeded).
|
|
3240
|
+
let base = '';
|
|
3241
|
+
let baseFetchError;
|
|
3242
|
+
// `baseRef` (branch/tag) and `baseSha` (raw commit) are mutually exclusive — a
|
|
3243
|
+
// caller picks one. If BOTH are set the envelope is ambiguous, so rather than
|
|
3244
|
+
// silently preferring one (a surprising `base...head` diff), skip the base
|
|
3245
|
+
// fetch and record a non-fatal diagnostic so the misconfiguration is visible.
|
|
3246
|
+
if (repo.baseSha && repo.baseRef) {
|
|
3247
|
+
baseFetchError = `ambiguous base: both baseRef (${repo.baseRef}) and baseSha (${repo.baseSha}) set — provide only one`;
|
|
3248
|
+
} else {
|
|
3249
|
+
const baseTarget = repo.baseSha || repo.baseRef;
|
|
3250
|
+
if (baseTarget) {
|
|
3251
|
+
const isBaseSha = !!repo.baseSha;
|
|
3252
|
+
const fetchArgs = [...credArgs(), 'fetch', '--no-tags'];
|
|
3253
|
+
if (repo.depth && repo.depth > 0) fetchArgs.push('--depth', String(repo.depth));
|
|
3254
|
+
if (repo.filter) fetchArgs.push(`--filter=${repo.filter}`);
|
|
3255
|
+
if (isBaseSha) {
|
|
3256
|
+
// A raw sha can't be mapped to a stable name — fetch it (updates FETCH_HEAD)
|
|
3257
|
+
// and expose the sha itself as the diff base.
|
|
3258
|
+
fetchArgs.push('origin', baseTarget);
|
|
3259
|
+
base = baseTarget;
|
|
3260
|
+
} else {
|
|
3261
|
+
// Map the branch onto refs/remotes/origin/<baseRef> so `origin/<base>`
|
|
3262
|
+
// resolves for the reviewer even on a single-branch clone.
|
|
3263
|
+
fetchArgs.push('origin', `+${baseTarget}:refs/remotes/origin/${baseTarget}`);
|
|
3264
|
+
base = `origin/${baseTarget}`;
|
|
3265
|
+
}
|
|
3266
|
+
const bf = runGit(fetchArgs, { cwd: workspaceDir, env: gitEnv, timeoutMs: effectiveTimeoutMs });
|
|
3267
|
+
if (bf.status !== 0) {
|
|
3268
|
+
// Name the base target (branch vs sha) so the warning is actionable when
|
|
3269
|
+
// multiple refs are in play, and preserve git's output/context in both the
|
|
3270
|
+
// timeout and non-timeout paths.
|
|
3271
|
+
const baseLabel = `${isBaseSha ? 'sha' : 'branch'} ${baseTarget}`;
|
|
3272
|
+
base = '';
|
|
3273
|
+
baseFetchError = bf.timedOut
|
|
3274
|
+
? `base fetch (${baseLabel}) timed out after ${effectiveTimeoutMs}ms: ${gitErrorDetail(bf, token, 300)}`
|
|
3275
|
+
: `base fetch (${baseLabel}) failed: ${gitErrorDetail(bf, token, 300)}`;
|
|
3276
|
+
}
|
|
3096
3277
|
}
|
|
3097
3278
|
}
|
|
3098
3279
|
|
|
@@ -3127,8 +3308,8 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
|
|
|
3127
3308
|
// finalizeGit skips the push/PR reconcile instead of pushing a bogus ref.
|
|
3128
3309
|
let workingBranch = null;
|
|
3129
3310
|
if (envelope.branch?.create) {
|
|
3130
|
-
const cb = runGit(['checkout', '-B', envelope.branch.create], { cwd: workspaceDir, env: gitEnv });
|
|
3131
|
-
if (cb.status !== 0) throw new ProvisionError(`git checkout -B ${envelope.branch.create}
|
|
3311
|
+
const cb = runGit(['checkout', '-B', envelope.branch.create], { cwd: workspaceDir, env: gitEnv, timeoutMs });
|
|
3312
|
+
if (cb.status !== 0) throw new ProvisionError(describeGitFailure(`git checkout -B ${envelope.branch.create}`, cb, { token, timeoutMs }));
|
|
3132
3313
|
workingBranch = envelope.branch.create;
|
|
3133
3314
|
} else {
|
|
3134
3315
|
const head = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: workspaceDir, env: gitEnv });
|
|
@@ -3139,7 +3320,7 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
|
|
|
3139
3320
|
// `git rev-parse HEAD` on an unborn branch (freshly cloned empty repo) exits
|
|
3140
3321
|
// non-zero and echoes the literal "HEAD" on stdout — treat that as "no base
|
|
3141
3322
|
// commit" (empty startSha) rather than a bogus revision.
|
|
3142
|
-
return { workspaceDir, gitEnv, committer, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref:
|
|
3323
|
+
return { workspaceDir, gitEnv, committer, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref: commitSha || branchName || '', base, baseFetchError, remote: redactToken(repo.url, token) };
|
|
3143
3324
|
}
|
|
3144
3325
|
|
|
3145
3326
|
// Look up a PR for this branch (2a does NOT open it — the harness does, driven
|
|
@@ -3278,9 +3459,10 @@ function finalizeGit({ workspaceDir, gitEnv, startSha, workingBranch, envelope,
|
|
|
3278
3459
|
if (!workingBranch) {
|
|
3279
3460
|
out.detached = true; // clone landed on a tag/sha ⇒ no branch to push
|
|
3280
3461
|
} else if (coerceBool(envelope.branch?.push, true) && out.commits.length > 0) {
|
|
3281
|
-
const
|
|
3462
|
+
const pushTimeoutMs = 120_000; // matches runGit's default; surfaced in a timeout reason
|
|
3463
|
+
const push = runGit([...credArgs(), 'push', '--set-upstream', 'origin', workingBranch], { cwd: workspaceDir, env: gitEnv, timeoutMs: pushTimeoutMs });
|
|
3282
3464
|
if (push.status === 0) out.pushed = true;
|
|
3283
|
-
else out.pushError =
|
|
3465
|
+
else out.pushError = describeGitFailure('git push', push, { token, timeoutMs: pushTimeoutMs });
|
|
3284
3466
|
}
|
|
3285
3467
|
|
|
3286
3468
|
if (workingBranch && envelope.task?.allowPr) {
|
|
@@ -4670,12 +4852,19 @@ async function workAgent(req, flags) {
|
|
|
4670
4852
|
runDir = mkdtempSync(join(agentRunsRoot(), 'run-'));
|
|
4671
4853
|
liveRunDirs.add(runDir);
|
|
4672
4854
|
provisioned = provisionRepo({ envelope, token: repoToken, runDir, timeoutMs: cloneTimeoutMs });
|
|
4855
|
+
if (provisioned.baseFetchError) {
|
|
4856
|
+
logger.warn(`[${jobType}] job ${job.jobKey} base fetch failed — ${provisioned.baseFetchError}; base...head diffs may be unavailable`);
|
|
4857
|
+
}
|
|
4673
4858
|
cwd = provisioned.workspaceDir;
|
|
4674
4859
|
extraEnv = {
|
|
4675
4860
|
AGENT_WORKSPACE: provisioned.workspaceDir,
|
|
4676
4861
|
AGENT_REPO_URL: provisioned.remote,
|
|
4677
4862
|
AGENT_REPO_BRANCH: provisioned.workingBranch || '',
|
|
4678
4863
|
AGENT_REPO_REF: provisioned.ref || '',
|
|
4864
|
+
// The fetched base ref (e.g. `origin/main` or a base sha) for
|
|
4865
|
+
// computing `git diff <base>...HEAD`; empty when none was requested
|
|
4866
|
+
// or the base fetch failed (see provisioned.baseFetchError).
|
|
4867
|
+
AGENT_REPO_BASE: provisioned.base || '',
|
|
4679
4868
|
// Pin the harness's commit identity to the resolved (placeholder-
|
|
4680
4869
|
// sanitized) committer so the agent's own `git commit` can't be
|
|
4681
4870
|
// hijacked by a placeholder GIT_AUTHOR_* inherited from process.env
|
|
@@ -8388,6 +8577,8 @@ export {
|
|
|
8388
8577
|
startLockExtender,
|
|
8389
8578
|
provisionRepo,
|
|
8390
8579
|
finalizeGit,
|
|
8580
|
+
describeGitFailure,
|
|
8581
|
+
boundGitOutput,
|
|
8391
8582
|
reconcileAgentPr,
|
|
8392
8583
|
resolveCommitterIdentity,
|
|
8393
8584
|
isPlaceholderEmail,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.36.0",
|
|
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.0",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.36.0",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.36.0",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.36.0",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.36.0",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.36.0",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.36.0"
|
|
67
67
|
}
|
|
68
68
|
}
|