c8ctl-plugin-nano 1.33.0 → 1.33.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/c8ctl-plugin.js +82 -10
- package/package.json +8 -8
package/c8ctl-plugin.js
CHANGED
|
@@ -2853,9 +2853,55 @@ function ghUserIdentity() {
|
|
|
2853
2853
|
}
|
|
2854
2854
|
}
|
|
2855
2855
|
|
|
2856
|
+
// Reject a commit-author email that can't be attributed to a real account and
|
|
2857
|
+
// can't receive mail — a `*@nano.local` (or other non-routable) placeholder
|
|
2858
|
+
// injected by the launch environment. Such an address produces UNVERIFIED
|
|
2859
|
+
// commits that look like a person but map to no GitHub user, so the harness must
|
|
2860
|
+
// never stamp it onto a commit; it falls through to the next identity source
|
|
2861
|
+
// instead. An EMPTY email is NOT a placeholder — it is an absent field handled
|
|
2862
|
+
// by ordinary per-field fallthrough, so it does not invalidate its source.
|
|
2863
|
+
// Matching is trim + case-insensitive.
|
|
2864
|
+
function isPlaceholderEmail(email) {
|
|
2865
|
+
const e = String(email || '').trim().toLowerCase();
|
|
2866
|
+
if (!e) return false; // absent — handled by per-field fallthrough, not a placeholder
|
|
2867
|
+
const at = e.lastIndexOf('@');
|
|
2868
|
+
if (at < 0) return true; // no domain at all — not a routable address
|
|
2869
|
+
const local = e.slice(0, at);
|
|
2870
|
+
const domain = e.slice(at + 1);
|
|
2871
|
+
// Malformed addresses missing a local part (`@example.com`) or a domain
|
|
2872
|
+
// (`user@`) can't be routed or attributed either — reject them too.
|
|
2873
|
+
if (!local || !domain) return true;
|
|
2874
|
+
// Non-routable mDNS/host-local TLDs and the loopback host: unattributable and
|
|
2875
|
+
// undeliverable, so never a legitimate commit author.
|
|
2876
|
+
return domain === 'localhost'
|
|
2877
|
+
|| domain.endsWith('.local')
|
|
2878
|
+
|| domain.endsWith('.internal');
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// Coerce one identity source into a usable { name, email }. When the source's
|
|
2882
|
+
// email is a non-routable placeholder we discard the WHOLE candidate (both
|
|
2883
|
+
// fields) rather than just the email — otherwise a placeholder-derived name
|
|
2884
|
+
// (e.g. `trial-merge`) would be stitched onto a borrowed email from a lower
|
|
2885
|
+
// source, forging a Frankenstein author. An empty email is preserved as-is so
|
|
2886
|
+
// ordinary per-field fill still works (e.g. git supplies a name, gh the email).
|
|
2887
|
+
// Fields are trimmed so a whitespace-only/space-padded name or email behaves
|
|
2888
|
+
// like "absent" (empty) rather than a truthy value that would block per-field
|
|
2889
|
+
// fallthrough and get stamped as an invalid commit identity — this matches
|
|
2890
|
+
// isPlaceholderEmail, which already normalizes via trim().
|
|
2891
|
+
function sanitizeIdentity(id) {
|
|
2892
|
+
const name = String((id && id.name) || '').trim();
|
|
2893
|
+
const email = String((id && id.email) || '').trim();
|
|
2894
|
+
if (isPlaceholderEmail(email)) return { name: '', email: '' };
|
|
2895
|
+
return { name, email };
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2856
2898
|
// Resolve the committer identity the harness stamps onto the cloned workspace.
|
|
2857
|
-
//
|
|
2899
|
+
// Source precedence: explicit GIT_AUTHOR_* env → the operator's global git
|
|
2858
2900
|
// config → the gh-authenticated GitHub user → the `nano-agent` fallback.
|
|
2901
|
+
// Precedence is per-field only for ABSENT fields (an empty name/email falls
|
|
2902
|
+
// through to the next source); a source whose email is a non-routable
|
|
2903
|
+
// placeholder is discarded WHOLE by sanitizeIdentity (name included), so in that
|
|
2904
|
+
// case its name does not participate in per-field fill (see sanitizeIdentity).
|
|
2859
2905
|
// Preferring the operator's real identity means autonomous commits are authored
|
|
2860
2906
|
// by the human running the fleet (who has signed any CLA) rather than an
|
|
2861
2907
|
// anonymous bot that hasn't; the agent's own authorship is recorded as a PR
|
|
@@ -2864,20 +2910,22 @@ function ghUserIdentity() {
|
|
|
2864
2910
|
// when a higher-precedence source didn't already supply the field — so explicit
|
|
2865
2911
|
// GIT_AUTHOR_* env fully short-circuits them (no `git config`/`gh` spawns, hence
|
|
2866
2912
|
// no added latency or failure modes when the override is present).
|
|
2867
|
-
//
|
|
2913
|
+
// Every candidate source is passed through sanitizeIdentity, so a non-routable
|
|
2914
|
+
// `*@nano.local` placeholder from ANY source (env, git-global) is discarded and
|
|
2915
|
+
// falls through to the gh identity / marked bot fallback — never stamped onto a
|
|
2916
|
+
// commit. `gitIdentity`/`ghIdentity` are injectable for testing.
|
|
2868
2917
|
function resolveCommitterIdentity({ gitIdentity = hostGitIdentity, ghIdentity = ghUserIdentity } = {}) {
|
|
2869
|
-
const
|
|
2870
|
-
const envEmail = process.env.GIT_AUTHOR_EMAIL || '';
|
|
2918
|
+
const env = sanitizeIdentity({ name: process.env.GIT_AUTHOR_NAME || '', email: process.env.GIT_AUTHOR_EMAIL || '' });
|
|
2871
2919
|
let g = null;
|
|
2872
|
-
const gitOnce = () => (g ??= (gitIdentity() || {
|
|
2920
|
+
const gitOnce = () => (g ??= sanitizeIdentity(gitIdentity() || {}));
|
|
2873
2921
|
let gh = null;
|
|
2874
|
-
const ghOnce = () => (gh ??= (ghIdentity() || {
|
|
2922
|
+
const ghOnce = () => (gh ??= sanitizeIdentity(ghIdentity() || {}));
|
|
2875
2923
|
|
|
2876
|
-
const name =
|
|
2877
|
-
const email =
|
|
2924
|
+
const name = env.name || gitOnce().name || ghOnce().name || 'nano-agent';
|
|
2925
|
+
const email = env.email || gitOnce().email || ghOnce().email || 'nano-agent@users.noreply.github.com';
|
|
2878
2926
|
|
|
2879
2927
|
const source =
|
|
2880
|
-
(
|
|
2928
|
+
(env.name || env.email) ? 'env'
|
|
2881
2929
|
: (g && (g.name || g.email)) ? 'git-global'
|
|
2882
2930
|
: (gh && (gh.name || gh.email)) ? 'gh'
|
|
2883
2931
|
: 'fallback';
|
|
@@ -2983,6 +3031,20 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
|
|
|
2983
3031
|
const committer = resolveCommitterIdentity();
|
|
2984
3032
|
runGit(['config', 'user.name', committer.name], { cwd: workspaceDir, env: gitEnv });
|
|
2985
3033
|
runGit(['config', 'user.email', committer.email], { cwd: workspaceDir, env: gitEnv });
|
|
3034
|
+
// Config alone is not enough: git honours GIT_AUTHOR_*/GIT_COMMITTER_* OVER
|
|
3035
|
+
// user.name/user.email config, so a placeholder GIT_AUTHOR_EMAIL inherited from
|
|
3036
|
+
// the launch environment (e.g. `trial-merge@nano.local`) would still be stamped
|
|
3037
|
+
// onto commits even though we just wrote a clean identity into config. Pin all
|
|
3038
|
+
// four env vars to the resolved (already placeholder-sanitized) identity so
|
|
3039
|
+
// EVERY commit — finalizeGit's own rebase commits (which run with gitEnv) and
|
|
3040
|
+
// the harness's commits (extraEnv below carries these into harnessEnv) — uses
|
|
3041
|
+
// it deterministically, and a non-routable `*@nano.local` author can never be
|
|
3042
|
+
// written. When GIT_AUTHOR_* already held a real identity, resolveCommitterIdentity
|
|
3043
|
+
// returned it verbatim, so this is a no-op in that case.
|
|
3044
|
+
gitEnv.GIT_AUTHOR_NAME = committer.name;
|
|
3045
|
+
gitEnv.GIT_AUTHOR_EMAIL = committer.email;
|
|
3046
|
+
gitEnv.GIT_COMMITTER_NAME = committer.name;
|
|
3047
|
+
gitEnv.GIT_COMMITTER_EMAIL = committer.email;
|
|
2986
3048
|
|
|
2987
3049
|
// Determine the working branch. With branch.create we make a real branch.
|
|
2988
3050
|
// Otherwise we're on whatever the clone checked out: a branch only if
|
|
@@ -3003,7 +3065,7 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
|
|
|
3003
3065
|
// `git rev-parse HEAD` on an unborn branch (freshly cloned empty repo) exits
|
|
3004
3066
|
// non-zero and echoes the literal "HEAD" on stdout — treat that as "no base
|
|
3005
3067
|
// commit" (empty startSha) rather than a bogus revision.
|
|
3006
|
-
return { workspaceDir, gitEnv, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref: target || '', remote: redactToken(repo.url, token) };
|
|
3068
|
+
return { workspaceDir, gitEnv, committer, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref: target || '', remote: redactToken(repo.url, token) };
|
|
3007
3069
|
}
|
|
3008
3070
|
|
|
3009
3071
|
// Look up a PR for this branch (2a does NOT open it — the harness does, driven
|
|
@@ -4239,6 +4301,15 @@ async function workAgent(req, flags) {
|
|
|
4239
4301
|
AGENT_REPO_URL: provisioned.remote,
|
|
4240
4302
|
AGENT_REPO_BRANCH: provisioned.workingBranch || '',
|
|
4241
4303
|
AGENT_REPO_REF: provisioned.ref || '',
|
|
4304
|
+
// Pin the harness's commit identity to the resolved (placeholder-
|
|
4305
|
+
// sanitized) committer so the agent's own `git commit` can't be
|
|
4306
|
+
// hijacked by a placeholder GIT_AUTHOR_* inherited from process.env
|
|
4307
|
+
// (git honours these over user.name/user.email config). Layered via
|
|
4308
|
+
// extraEnv so they override any inherited placeholder in harnessEnv.
|
|
4309
|
+
GIT_AUTHOR_NAME: provisioned.committer.name,
|
|
4310
|
+
GIT_AUTHOR_EMAIL: provisioned.committer.email,
|
|
4311
|
+
GIT_COMMITTER_NAME: provisioned.committer.name,
|
|
4312
|
+
GIT_COMMITTER_EMAIL: provisioned.committer.email,
|
|
4242
4313
|
};
|
|
4243
4314
|
} catch (err) {
|
|
4244
4315
|
if (runDir) { try { rmSync(runDir, { recursive: true, force: true }); } catch { /* best effort */ } liveRunDirs.delete(runDir); }
|
|
@@ -7506,6 +7577,7 @@ export {
|
|
|
7506
7577
|
finalizeGit,
|
|
7507
7578
|
reconcileAgentPr,
|
|
7508
7579
|
resolveCommitterIdentity,
|
|
7580
|
+
isPlaceholderEmail,
|
|
7509
7581
|
postAgentAttribution,
|
|
7510
7582
|
reapAgentRunDirs,
|
|
7511
7583
|
authUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.33.
|
|
3
|
+
"version": "1.33.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.33.
|
|
61
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.33.
|
|
62
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.33.
|
|
63
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.33.
|
|
64
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.33.
|
|
65
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.33.
|
|
66
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.33.
|
|
60
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.33.1",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.33.1",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.33.1",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.33.1",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.33.1",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.33.1",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.33.1"
|
|
67
67
|
}
|
|
68
68
|
}
|