c8ctl-plugin-nano 1.26.0 → 1.26.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.
Files changed (2) hide show
  1. package/c8ctl-plugin.js +116 -8
  2. package/package.json +8 -8
package/c8ctl-plugin.js CHANGED
@@ -2295,14 +2295,9 @@ function makeSecretResolver(kind) {
2295
2295
  // Resolve the names a job needs (setup.secretRefs, plus the repo/PR credential
2296
2296
  // when allowPr). Returns resolved values + a list of names that were missing so
2297
2297
  // the caller can fail the job with a clear provisioning error.
2298
- function resolveJobSecrets(resolver, envelope) {
2298
+ function resolveJobSecrets(resolver, envelope, { ghAuthToken = ghAuthTokenFromCli } = {}) {
2299
2299
  const names = new Set();
2300
2300
  for (const n of envelope.setup?.secretRefs || []) if (n) names.add(n);
2301
- if (envelope.task?.allowPr) {
2302
- const provider = envelope.repository?.provider || 'github';
2303
- const authRef = envelope.repository?.authRef || (provider === 'github' ? 'GITHUB_TOKEN' : undefined);
2304
- if (authRef) names.add(authRef);
2305
- }
2306
2301
  const resolved = {};
2307
2302
  const missing = [];
2308
2303
  for (const name of names) {
@@ -2310,6 +2305,34 @@ function resolveJobSecrets(resolver, envelope) {
2310
2305
  if (v === undefined) missing.push(name);
2311
2306
  else resolved[name] = v;
2312
2307
  }
2308
+ // The github clone/push credential is resolved with a gh-CLI fallback so a
2309
+ // default GITHUB_TOKEN isn't reported "missing" merely because it isn't in the
2310
+ // env when `gh auth login` provides it. A custom authRef stays strict.
2311
+ if (envelope.task?.allowPr) {
2312
+ const provider = envelope.repository?.provider || 'github';
2313
+ const authRef = envelope.repository?.authRef;
2314
+ const ref = normalizeAuthRef(authRef);
2315
+ if (ref.kind === 'invalid') {
2316
+ // A present-but-blank authRef is a misconfiguration: surface it as missing
2317
+ // so provisioning sheds rather than silently borrowing the default/gh token.
2318
+ if (!missing.includes('repository.authRef')) missing.push('repository.authRef');
2319
+ } else {
2320
+ const ghAuthRef = ref.kind === 'custom'
2321
+ ? ref.name
2322
+ : (provider === 'github' ? 'GITHUB_TOKEN' : undefined);
2323
+ if (ghAuthRef) {
2324
+ names.add(ghAuthRef);
2325
+ const token = githubCloneToken({ provider, authRef, secretResolver: resolver, ghAuthToken });
2326
+ const missingIdx = missing.indexOf(ghAuthRef);
2327
+ if (token) {
2328
+ resolved[ghAuthRef] = token;
2329
+ if (missingIdx !== -1) missing.splice(missingIdx, 1);
2330
+ } else if (missingIdx === -1) {
2331
+ missing.push(ghAuthRef);
2332
+ }
2333
+ }
2334
+ }
2335
+ }
2313
2336
  return { resolved, missing, names: [...names] };
2314
2337
  }
2315
2338
 
@@ -2460,6 +2483,82 @@ function credArgs() {
2460
2483
  return ['-c', 'credential.helper='];
2461
2484
  }
2462
2485
 
2486
+ // Fall back to the `gh` CLI's stored credential when GITHUB_TOKEN is not exported
2487
+ // to the env. Most interactive setups authenticate with `gh auth login` (keychain)
2488
+ // rather than an env var, so an env-only secret resolver yields no token and a
2489
+ // private/internal clone fails with "could not read Username". Best effort: returns
2490
+ // a trimmed token, or null when gh is missing / not logged in. The token is fed to
2491
+ // git via GIT_ASKPASS only (never argv/URL/helper), preserving the ephemeral-token
2492
+ // guarantee.
2493
+ // Memoized for the process lifetime: this is a synchronous spawnSync (up to a
2494
+ // 10s timeout) that can be reached per job, and jobs may run concurrently
2495
+ // (maxParallelJobs > 1), so consult the CLI at most once per worker run rather
2496
+ // than blocking every handler. A sentinel distinguishes "not yet computed" from
2497
+ // a cached null (gh missing / not logged in).
2498
+ //
2499
+ // Memoization alone still lets the *first* job pay the synchronous spawn on the
2500
+ // event loop, stalling any sibling handlers (and lock-extension heartbeats) for
2501
+ // up to the timeout. So `nano work` primes this cache once at startup via
2502
+ // `primeGhAuthToken()` — before the poll loop — moving the one unavoidable
2503
+ // blocking spawn off the job-handling path entirely. Any later call is a warm
2504
+ // cache hit; the memoization here is the safety net for paths that never primed.
2505
+ const GH_AUTH_TOKEN_UNSET = Symbol('gh-auth-token-unset');
2506
+ let ghAuthTokenCache = GH_AUTH_TOKEN_UNSET;
2507
+ function ghAuthTokenFromCli() {
2508
+ if (ghAuthTokenCache !== GH_AUTH_TOKEN_UNSET) return ghAuthTokenCache;
2509
+ let token = null;
2510
+ try {
2511
+ const r = spawnSync('gh', ['auth', 'token'], { encoding: 'utf8', timeout: 10_000 });
2512
+ const tok = r.status === 0 ? (r.stdout || '').trim() : '';
2513
+ token = tok || null;
2514
+ } catch {
2515
+ token = null;
2516
+ }
2517
+ ghAuthTokenCache = token;
2518
+ return token;
2519
+ }
2520
+
2521
+ // Warm the gh-token cache once, off the job-handling path. Safe to call any
2522
+ // number of times: the first call performs the (possibly blocking) lookup, the
2523
+ // rest are cache hits. Returns true once the cache is populated.
2524
+ function primeGhAuthToken() {
2525
+ ghAuthTokenFromCli();
2526
+ return ghAuthTokenCache !== GH_AUTH_TOKEN_UNSET;
2527
+ }
2528
+
2529
+ // Normalize a repository authRef into one of three intents. Trimming matters so
2530
+ // a present-but-blank authRef ('' or whitespace) is treated as a misconfiguration
2531
+ // rather than "absent": absence enables the default/gh fallback, but a blank
2532
+ // custom ref must NOT silently borrow the operator's gh login.
2533
+ // { kind: 'default' } no custom authRef configured (undefined/null)
2534
+ // { kind: 'custom', name } a non-empty custom authRef (strict)
2535
+ // { kind: 'invalid' } authRef present but blank (config error)
2536
+ function normalizeAuthRef(authRef) {
2537
+ if (authRef === undefined || authRef === null) return { kind: 'default' };
2538
+ const trimmed = String(authRef).trim();
2539
+ if (trimmed === '') return { kind: 'invalid' };
2540
+ return { kind: 'custom', name: trimmed };
2541
+ }
2542
+
2543
+ // Resolve the github clone/push credential. The default credential (env
2544
+ // GITHUB_TOKEN) falls back to the gh CLI's stored token so `gh auth login`
2545
+ // setups work without exporting GITHUB_TOKEN. A custom `authRef` is honored
2546
+ // strictly (env/secret resolver only, no gh fallback) so a misconfigured named
2547
+ // secret surfaces as missing rather than silently borrowing the operator's gh
2548
+ // login. An authRef that is present but blank is a misconfiguration and yields no
2549
+ // token (never the gh fallback). `ghAuthToken` is injectable for testing.
2550
+ // Returns a token or null.
2551
+ function githubCloneToken({ provider, authRef, secretResolver, ghAuthToken = ghAuthTokenFromCli }) {
2552
+ const prov = provider || 'github';
2553
+ const ref = normalizeAuthRef(authRef);
2554
+ if (ref.kind === 'invalid') return null;
2555
+ const usesDefault = prov === 'github' && ref.kind === 'default';
2556
+ const name = ref.kind === 'custom' ? ref.name : (prov === 'github' ? 'GITHUB_TOKEN' : null);
2557
+ let token = name ? (secretResolver.resolve(name) || null) : null;
2558
+ if (!token && usesDefault) token = ghAuthToken() || null;
2559
+ return token;
2560
+ }
2561
+
2463
2562
  // Clone repo into <runDir>/workspace and check out / create the working branch.
2464
2563
  // Returns { workspaceDir, gitEnv, startSha, workingBranch, remote }. Throws a
2465
2564
  // ProvisionError (token-redacted) on any git failure so the caller can shed.
@@ -3194,6 +3293,12 @@ async function workAgent(req, flags) {
3194
3293
  const extraNote = extraJobTypes.length > 0 ? ` (${extraJobTypes.length} via --job-type)` : '';
3195
3294
  logger.info(` listening on ${jobTypes.length} job type(s)${extraNote}: ${jobTypes.join(' ')}`);
3196
3295
  logger.info(` max parallel: ${maxParallelJobs}; recovery window: ${recoveryWindowMs}ms; idle timeout: ${idleTimeoutMs}ms; hard cap: ${hardCapMs > 0 ? `${hardCapMs}ms` : 'off'}; poll timeout: ${pollTimeoutMs}ms`);
3296
+ // Warm the gh-token cache now, off the job-handling path: githubCloneToken()
3297
+ // may consult `gh auth token` (a synchronous spawn, up to 10s) as its default
3298
+ // credential fallback, and doing that inside a job handler would stall sibling
3299
+ // handlers + lock heartbeats when maxParallelJobs > 1. Priming here pays that
3300
+ // cost once at startup so every later lookup is a warm cache hit.
3301
+ primeGhAuthToken();
3197
3302
  logger.info('Polling for work — press Ctrl-C to stop.');
3198
3303
 
3199
3304
  // When launched under the supervisor, report per-job activity — which job(s)
@@ -3295,8 +3400,8 @@ async function workAgent(req, flags) {
3295
3400
  let repoToken = null;
3296
3401
  if (hasRepo) {
3297
3402
  const provider = envelope.repository.provider || 'github';
3298
- const authRef = envelope.repository.authRef || (provider === 'github' ? 'GITHUB_TOKEN' : null);
3299
- if (authRef) repoToken = secretResolver.resolve(authRef) || null; // optional: absent → anonymous clone
3403
+ const authRef = envelope.repository.authRef;
3404
+ repoToken = githubCloneToken({ provider, authRef, secretResolver }); // absent → anonymous clone
3300
3405
  try {
3301
3406
  mkdirSync(agentRunsRoot(), { recursive: true });
3302
3407
  runDir = mkdtempSync(join(agentRunsRoot(), 'run-'));
@@ -6217,6 +6322,9 @@ export {
6217
6322
  reconcileAgentPr,
6218
6323
  reapAgentRunDirs,
6219
6324
  authUrl,
6325
+ githubCloneToken,
6326
+ ghAuthTokenFromCli,
6327
+ primeGhAuthToken,
6220
6328
  redactToken,
6221
6329
  agentRunsRoot,
6222
6330
  ProvisionError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.26.0",
3
+ "version": "1.26.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",
@@ -47,12 +47,12 @@
47
47
  "semantic-release": "^25.0.3"
48
48
  },
49
49
  "optionalDependencies": {
50
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.26.0",
51
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.26.0",
52
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.26.0",
53
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.26.0",
54
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.26.0",
55
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.26.0",
56
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.26.0"
50
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.26.1",
51
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.26.1",
52
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.26.1",
53
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.26.1",
54
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.26.1",
55
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.26.1",
56
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.26.1"
57
57
  }
58
58
  }