@oscharko-dev/keiko-tools 0.2.8 → 0.2.9
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/dist/.tsbuildinfo +1 -1
- package/dist/git-commit-intent-node.d.ts +7 -0
- package/dist/git-commit-intent-node.d.ts.map +1 -0
- package/dist/git-commit-intent-node.js +42 -0
- package/dist/git-merge-gateway.d.ts +95 -0
- package/dist/git-merge-gateway.d.ts.map +1 -0
- package/dist/git-merge-gateway.js +536 -0
- package/dist/git-merge-node.d.ts +17 -0
- package/dist/git-merge-node.d.ts.map +1 -0
- package/dist/git-merge-node.js +243 -0
- package/dist/git-mutation-adapter.d.ts +51 -0
- package/dist/git-mutation-adapter.d.ts.map +1 -0
- package/dist/git-mutation-adapter.js +207 -0
- package/dist/git-mutation-evidence.d.ts +23 -0
- package/dist/git-mutation-evidence.d.ts.map +1 -0
- package/dist/git-mutation-evidence.js +283 -0
- package/dist/git-mutation-node.d.ts +22 -0
- package/dist/git-mutation-node.d.ts.map +1 -0
- package/dist/git-mutation-node.js +145 -0
- package/dist/git-mutation-orchestrator.d.ts +92 -0
- package/dist/git-mutation-orchestrator.d.ts.map +1 -0
- package/dist/git-mutation-orchestrator.js +321 -0
- package/dist/git-mutation-preflight.d.ts +35 -0
- package/dist/git-mutation-preflight.d.ts.map +1 -0
- package/dist/git-mutation-preflight.js +226 -0
- package/dist/git-mutation-taxonomy.d.ts +15 -0
- package/dist/git-mutation-taxonomy.d.ts.map +1 -0
- package/dist/git-mutation-taxonomy.js +102 -0
- package/dist/git-pr-gateway.d.ts +101 -0
- package/dist/git-pr-gateway.d.ts.map +1 -0
- package/dist/git-pr-gateway.js +487 -0
- package/dist/git-pr-node.d.ts +17 -0
- package/dist/git-pr-node.d.ts.map +1 -0
- package/dist/git-pr-node.js +173 -0
- package/dist/git-publish-gateway.d.ts +72 -0
- package/dist/git-publish-gateway.d.ts.map +1 -0
- package/dist/git-publish-gateway.js +423 -0
- package/dist/git-publish-node.d.ts +17 -0
- package/dist/git-publish-node.d.ts.map +1 -0
- package/dist/git-publish-node.js +107 -0
- package/dist/git-worktree-adapter.d.ts +78 -0
- package/dist/git-worktree-adapter.d.ts.map +1 -0
- package/dist/git-worktree-adapter.js +300 -0
- package/dist/git-worktree-snapshot-node.d.ts +31 -0
- package/dist/git-worktree-snapshot-node.d.ts.map +1 -0
- package/dist/git-worktree-snapshot-node.js +189 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -0
- package/package.json +9 -5
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { GitDeliveryApprovalRequirement, GitDeliveryBlockReason, GitDeliveryExecutionErrorCode, GitDeliveryExecutionResult, GitDeliveryOrgPolicyPack, GitDeliveryPolicyDecision, GitDeliveryProviderCapability, GitDeliveryPushInputs, GitDeliveryRecoveryActionHint, GitDeliveryRecoveryDisposition, GitDeliveryRepoPolicyPack } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
import type { GitWorktreeSnapshot } from "./git-mutation-preflight.js";
|
|
3
|
+
import type { GitMutationLifecycleResult } from "./git-mutation-orchestrator.js";
|
|
4
|
+
import type { CommandRule } from "./types.js";
|
|
5
|
+
export interface GitPushCommand {
|
|
6
|
+
readonly kind: "push";
|
|
7
|
+
readonly sourceBranchName: string;
|
|
8
|
+
readonly remoteAlias: string;
|
|
9
|
+
readonly remoteBranchName: string;
|
|
10
|
+
readonly forcePush: boolean;
|
|
11
|
+
readonly setUpstreamTracking: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface GitPublishExecRequest {
|
|
14
|
+
readonly sourceBranchName: string;
|
|
15
|
+
readonly remoteAlias: string;
|
|
16
|
+
readonly remoteBranchName: string;
|
|
17
|
+
readonly setUpstreamTracking: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface GitPublishExecResult extends GitDeliveryExecutionResult {
|
|
20
|
+
readonly rejectionReason?: GitPublishRejectionReason | undefined;
|
|
21
|
+
}
|
|
22
|
+
export interface GitRemotePublishAdapter {
|
|
23
|
+
publish(req: GitPublishExecRequest): Promise<GitPublishExecResult>;
|
|
24
|
+
}
|
|
25
|
+
export type GitPublishRejectionReason = "non-fast-forward" | "fetch-first" | "no-upstream" | "auth-failed" | "permission-denied" | "protected-ref" | "remote-unavailable" | "unknown";
|
|
26
|
+
export declare const GIT_PUBLISH_REJECTION_REASONS: readonly GitPublishRejectionReason[];
|
|
27
|
+
export declare function isGitPublishRejectionReason(value: unknown): value is GitPublishRejectionReason;
|
|
28
|
+
export declare function classifyGitPublishRejection(output: string): GitPublishRejectionReason;
|
|
29
|
+
export declare function gitPublishRejectionToErrorCode(reason: GitPublishRejectionReason): GitDeliveryExecutionErrorCode;
|
|
30
|
+
export interface GitPublishRejection {
|
|
31
|
+
readonly reason: GitPublishRejectionReason;
|
|
32
|
+
readonly disposition: GitDeliveryRecoveryDisposition;
|
|
33
|
+
readonly actionHint?: GitDeliveryRecoveryActionHint | undefined;
|
|
34
|
+
}
|
|
35
|
+
export declare function gitPublishRejectionFor(reason: GitPublishRejectionReason): GitPublishRejection;
|
|
36
|
+
export declare const GIT_PUBLISH_ALLOWED_SUBCOMMANDS: readonly string[];
|
|
37
|
+
export declare const GIT_PUBLISH_COMMAND_RULES: readonly CommandRule[];
|
|
38
|
+
export declare class GitPublishArgvError extends Error {
|
|
39
|
+
constructor(message: string);
|
|
40
|
+
}
|
|
41
|
+
export declare function buildPushArgv(command: GitPushCommand): readonly string[];
|
|
42
|
+
export interface GitPublishRequest {
|
|
43
|
+
readonly command: GitPushCommand;
|
|
44
|
+
readonly approval: GitDeliveryApprovalRequirement;
|
|
45
|
+
}
|
|
46
|
+
export interface GitPublishOrchestratorDeps {
|
|
47
|
+
readonly adapter: GitRemotePublishAdapter;
|
|
48
|
+
readonly snapshot: GitWorktreeSnapshot;
|
|
49
|
+
readonly orgPolicyPack?: GitDeliveryOrgPolicyPack | undefined;
|
|
50
|
+
readonly repoPolicyPack?: GitDeliveryRepoPolicyPack | undefined;
|
|
51
|
+
readonly activeProviderCapabilities?: readonly GitDeliveryProviderCapability[] | undefined;
|
|
52
|
+
readonly now: () => number;
|
|
53
|
+
readonly newActionId: () => string;
|
|
54
|
+
}
|
|
55
|
+
export interface GitPublishLifecycleResult {
|
|
56
|
+
readonly lifecycle: GitMutationLifecycleResult;
|
|
57
|
+
readonly rejection?: GitPublishRejection | undefined;
|
|
58
|
+
}
|
|
59
|
+
export interface GitPublishEffectivePolicy {
|
|
60
|
+
readonly outcome: "allowed" | "blocked" | "approval-gated";
|
|
61
|
+
readonly blockReason?: GitDeliveryBlockReason | undefined;
|
|
62
|
+
}
|
|
63
|
+
export declare function evaluateGitPublishEffectivePolicy(decision: GitDeliveryPolicyDecision, target: string | undefined, capabilities: readonly GitDeliveryProviderCapability[], pushInputs: GitDeliveryPushInputs): GitPublishEffectivePolicy;
|
|
64
|
+
/**
|
|
65
|
+
* Runs ONE governed publish end-to-end: derive push inputs → preflight (push case) → preview → policy →
|
|
66
|
+
* (only when preflight passes, policy permits, approval satisfied) execute through the narrow remote
|
|
67
|
+
* adapter. Returns a kernel-shaped lifecycle result (so the #474 evidence builder records it unchanged)
|
|
68
|
+
* plus the live publish-rejection descriptor when the remote rejected an executed push.
|
|
69
|
+
*/
|
|
70
|
+
export declare function runGitPublish(request: GitPublishRequest, deps: GitPublishOrchestratorDeps): Promise<GitPublishLifecycleResult>;
|
|
71
|
+
export declare function gitPublishArgvIsGoverned(argv: readonly string[]): boolean;
|
|
72
|
+
//# sourceMappingURL=git-publish-gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-publish-gateway.d.ts","sourceRoot":"","sources":["../src/git-publish-gateway.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAGV,8BAA8B,EAC9B,sBAAsB,EAEtB,6BAA6B,EAC7B,0BAA0B,EAC1B,wBAAwB,EAExB,yBAAyB,EACzB,6BAA6B,EAC7B,qBAAqB,EACrB,6BAA6B,EAC7B,8BAA8B,EAC9B,yBAAyB,EAC1B,MAAM,+BAA+B,CAAC;AAQvC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAEvE,OAAO,KAAK,EACV,0BAA0B,EAE3B,MAAM,gCAAgC,CAAC;AAGxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAO9C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAIlC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;CACvC;AAID,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;CACvC;AAID,MAAM,WAAW,oBAAqB,SAAQ,0BAA0B;IACtE,QAAQ,CAAC,eAAe,CAAC,EAAE,yBAAyB,GAAG,SAAS,CAAC;CAClE;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,GAAG,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACpE;AAOD,MAAM,MAAM,yBAAyB,GACjC,kBAAkB,GAClB,aAAa,GACb,aAAa,GACb,aAAa,GACb,mBAAmB,GACnB,eAAe,GACf,oBAAoB,GACpB,SAAS,CAAC;AAEd,eAAO,MAAM,6BAA6B,EAAE,SAAS,yBAAyB,EASpE,CAAC;AAEX,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,yBAAyB,CAK9F;AAkDD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,yBAAyB,CAQrF;AAmBD,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,yBAAyB,GAChC,6BAA6B,CAE/B;AAgCD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,yBAAyB,CAAC;IAC3C,QAAQ,CAAC,WAAW,EAAE,8BAA8B,CAAC;IACrD,QAAQ,CAAC,UAAU,CAAC,EAAE,6BAA6B,GAAG,SAAS,CAAC;CACjE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,yBAAyB,GAAG,mBAAmB,CAO7F;AAOD,eAAO,MAAM,+BAA+B,EAAE,SAAS,MAAM,EAA4B,CAAC;AAE1F,eAAO,MAAM,yBAAyB,EAAE,SAAS,WAAW,EAmC1D,CAAC;AAEH,qBAAa,mBAAoB,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAInC;AA8BD,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,MAAM,EAAE,CAaxE;AAID,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,8BAA8B,CAAC;CACnD;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAC1C,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,EAAE,wBAAwB,GAAG,SAAS,CAAC;IAC9D,QAAQ,CAAC,cAAc,CAAC,EAAE,yBAAyB,GAAG,SAAS,CAAC;IAChE,QAAQ,CAAC,0BAA0B,CAAC,EAAE,SAAS,6BAA6B,EAAE,GAAG,SAAS,CAAC;IAC3F,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,MAAM,CAAC;CACpC;AAKD,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,SAAS,EAAE,0BAA0B,CAAC;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;CACtD;AAqGD,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,gBAAgB,CAAC;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;CAC3D;AAED,wBAAgB,iCAAiC,CAC/C,QAAQ,EAAE,yBAAyB,EACnC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,YAAY,EAAE,SAAS,6BAA6B,EAAE,EACtD,UAAU,EAAE,qBAAqB,GAChC,yBAAyB,CAiB3B;AA0HD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,0BAA0B,GAC/B,OAAO,CAAC,yBAAyB,CAAC,CA0CpC;AAID,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAEzE"}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// The governed remote publish gateway (Issue #476, Epic #470) — AC1–AC5.
|
|
2
|
+
//
|
|
3
|
+
// This is the SEPARATE remote execution authority the #472 kernel deferred: its command-union comment
|
|
4
|
+
// places remote kinds in "later slices (#476–#478) that extend this union and register their executors",
|
|
5
|
+
// and its local adapter allowlist excludes network verbs because "remote and provider execution is a
|
|
6
|
+
// later slice (#476–#478) behind a SEPARATE gateway, never this local adapter". This module is that
|
|
7
|
+
// gateway. It NEVER touches the local mutation adapter or its allowlist.
|
|
8
|
+
//
|
|
9
|
+
// Like the local kernel, the gateway is deterministic given its injected dependencies (snapshot, clock,
|
|
10
|
+
// id generator, remote adapter). It performs no IO of its own: the actual `git push` lives behind the
|
|
11
|
+
// injected GitRemotePublishAdapter (implemented by git-publish-node.ts on the Node subpath), so the
|
|
12
|
+
// orchestrator is unit-testable with a fake adapter and never opens a parallel child_process path.
|
|
13
|
+
//
|
|
14
|
+
// Pure module except for awaiting the injected adapter. Reuses the kernel's pure machinery unchanged:
|
|
15
|
+
// evaluateGitPreflight (push case), evaluateGitPolicy, the GitMutationLifecycleResult shape (so the
|
|
16
|
+
// #474 evidence builder consumes a push lifecycle with no change), and the failure taxonomy.
|
|
17
|
+
import { evaluateGitPolicy, GIT_DELIVERY_RISK_CLASS_SEVERITY, GIT_DELIVERY_SCHEMA_VERSION, gitDeliveryBranchNameMatchesAny, gitDeliveryRiskClassForInputs, } from "@oscharko-dev/keiko-contracts";
|
|
18
|
+
import { evaluateGitPreflight } from "./git-mutation-preflight.js";
|
|
19
|
+
import { gitMutationCategoryForExecutionResult } from "./git-mutation-taxonomy.js";
|
|
20
|
+
export const GIT_PUBLISH_REJECTION_REASONS = [
|
|
21
|
+
"non-fast-forward",
|
|
22
|
+
"fetch-first",
|
|
23
|
+
"no-upstream",
|
|
24
|
+
"auth-failed",
|
|
25
|
+
"permission-denied",
|
|
26
|
+
"protected-ref",
|
|
27
|
+
"remote-unavailable",
|
|
28
|
+
"unknown",
|
|
29
|
+
];
|
|
30
|
+
export function isGitPublishRejectionReason(value) {
|
|
31
|
+
return (typeof value === "string" &&
|
|
32
|
+
GIT_PUBLISH_REJECTION_REASONS.includes(value));
|
|
33
|
+
}
|
|
34
|
+
// Ordered phrase table. The first reason whose any-phrase appears in the (lower-cased, secret-redacted)
|
|
35
|
+
// git output wins, so specific causes are matched before generic ones. Phrases are SPECIFIC: the generic
|
|
36
|
+
// "failed to push some refs" / "could not read from remote repository" lines git prints for many failures
|
|
37
|
+
// are deliberately NOT used as discriminators — only the cause-bearing hint lines are.
|
|
38
|
+
const REJECTION_PHRASES = [
|
|
39
|
+
["non-fast-forward", ["non-fast-forward", "tip of your current branch is behind"]],
|
|
40
|
+
["fetch-first", ["fetch first", "remote contains work that you do"]],
|
|
41
|
+
[
|
|
42
|
+
"protected-ref",
|
|
43
|
+
["protected branch", "pre-receive hook declined", "refusing to update", "gh006"],
|
|
44
|
+
],
|
|
45
|
+
// Auth markers are checked BEFORE the generic "permission denied": an SSH "Permission denied
|
|
46
|
+
// (publickey)" is an AUTHENTICATION failure, whereas "remote: Permission to repo denied to user" is
|
|
47
|
+
// an authorization failure caught just below.
|
|
48
|
+
// Auth includes smart-HTTP 401 ("returned error: 401"), which some hosts emit without a remote: line.
|
|
49
|
+
[
|
|
50
|
+
"auth-failed",
|
|
51
|
+
[
|
|
52
|
+
"authentication failed",
|
|
53
|
+
"could not read username",
|
|
54
|
+
"publickey",
|
|
55
|
+
"invalid username",
|
|
56
|
+
"error: 401",
|
|
57
|
+
],
|
|
58
|
+
],
|
|
59
|
+
// Permission includes smart-HTTP 403 ("returned error: 403"): git prints `unable to access ... returned
|
|
60
|
+
// error: 403` with NO literal "forbidden", and some hosts/proxies omit the `remote: Permission` line, so
|
|
61
|
+
// the bare 403 token must classify as an authorization denial (user-fixable) rather than falling through
|
|
62
|
+
// to remote-unavailable (retryable).
|
|
63
|
+
[
|
|
64
|
+
"permission-denied",
|
|
65
|
+
["permission denied", "permission to", "error: 403", "403 forbidden", "access denied"],
|
|
66
|
+
],
|
|
67
|
+
["no-upstream", ["has no upstream branch", "no upstream configured", "set-upstream"]],
|
|
68
|
+
[
|
|
69
|
+
"remote-unavailable",
|
|
70
|
+
[
|
|
71
|
+
"could not resolve host",
|
|
72
|
+
"could not read from remote",
|
|
73
|
+
"connection refused",
|
|
74
|
+
"timed out",
|
|
75
|
+
"unable to access",
|
|
76
|
+
],
|
|
77
|
+
],
|
|
78
|
+
];
|
|
79
|
+
// Pure classifier. Deterministic: same output text always yields the same reason. Matches lower-cased
|
|
80
|
+
// so capitalisation differences across git versions do not change the classification.
|
|
81
|
+
export function classifyGitPublishRejection(output) {
|
|
82
|
+
const haystack = output.toLowerCase();
|
|
83
|
+
for (const [reason, phrases] of REJECTION_PHRASES) {
|
|
84
|
+
if (phrases.some((phrase) => haystack.includes(phrase))) {
|
|
85
|
+
return reason;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return "unknown";
|
|
89
|
+
}
|
|
90
|
+
// ─── Reason → contract error code + recovery (reuses #471/#473/#474 vocabularies) ───────────
|
|
91
|
+
// The error code is the content-free token recorded in evidence; the recovery composes the reused
|
|
92
|
+
// three-way disposition with an action hint where one fits the existing vocabulary cleanly.
|
|
93
|
+
const REJECTION_ERROR_CODE = {
|
|
94
|
+
"non-fast-forward": "precondition-failed",
|
|
95
|
+
"fetch-first": "precondition-failed",
|
|
96
|
+
"no-upstream": "precondition-failed",
|
|
97
|
+
"auth-failed": "provider-rejected",
|
|
98
|
+
"permission-denied": "provider-rejected",
|
|
99
|
+
"protected-ref": "provider-rejected",
|
|
100
|
+
"remote-unavailable": "network-failure",
|
|
101
|
+
unknown: "provider-rejected",
|
|
102
|
+
};
|
|
103
|
+
export function gitPublishRejectionToErrorCode(reason) {
|
|
104
|
+
return REJECTION_ERROR_CODE[reason];
|
|
105
|
+
}
|
|
106
|
+
const REJECTION_DISPOSITION = {
|
|
107
|
+
"non-fast-forward": "user-fixable",
|
|
108
|
+
"fetch-first": "user-fixable",
|
|
109
|
+
"no-upstream": "user-fixable",
|
|
110
|
+
"auth-failed": "user-fixable",
|
|
111
|
+
"permission-denied": "user-fixable",
|
|
112
|
+
"protected-ref": "user-fixable",
|
|
113
|
+
"remote-unavailable": "retryable",
|
|
114
|
+
unknown: "user-fixable",
|
|
115
|
+
};
|
|
116
|
+
// Action hint only where the #473 vocabulary fits cleanly; auth/permission/unknown intentionally carry
|
|
117
|
+
// NO hint (the precise rejectionReason is the user-facing signal) rather than a misleading one.
|
|
118
|
+
const REJECTION_ACTION_HINT = {
|
|
119
|
+
"non-fast-forward": "resolve-conflicts",
|
|
120
|
+
"fetch-first": "resolve-conflicts",
|
|
121
|
+
"no-upstream": "configure-upstream",
|
|
122
|
+
"auth-failed": undefined,
|
|
123
|
+
"permission-denied": undefined,
|
|
124
|
+
"protected-ref": "adjust-policy-target",
|
|
125
|
+
"remote-unavailable": "retry",
|
|
126
|
+
unknown: undefined,
|
|
127
|
+
};
|
|
128
|
+
export function gitPublishRejectionFor(reason) {
|
|
129
|
+
const actionHint = REJECTION_ACTION_HINT[reason];
|
|
130
|
+
return {
|
|
131
|
+
reason,
|
|
132
|
+
disposition: REJECTION_DISPOSITION[reason],
|
|
133
|
+
...(actionHint !== undefined ? { actionHint } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// ─── Dedicated push allowlist + pure argv builder (force refused) ────────────────────────────
|
|
137
|
+
// A closed allowlist permitting ONLY `push`. Structurally separate from the local mutation rules and
|
|
138
|
+
// the read-only inspection rules. Mirrors their defence-in-depth flag denials so a smuggled global flag
|
|
139
|
+
// is rejected before spawn even though argv is built only from typed operands.
|
|
140
|
+
export const GIT_PUBLISH_ALLOWED_SUBCOMMANDS = Object.freeze(["push"]);
|
|
141
|
+
export const GIT_PUBLISH_COMMAND_RULES = Object.freeze([
|
|
142
|
+
{
|
|
143
|
+
executable: "git",
|
|
144
|
+
allowedSubcommands: GIT_PUBLISH_ALLOWED_SUBCOMMANDS,
|
|
145
|
+
valueFlags: Object.freeze([
|
|
146
|
+
"-C",
|
|
147
|
+
"-c",
|
|
148
|
+
"--git-dir",
|
|
149
|
+
"--work-tree",
|
|
150
|
+
"--namespace",
|
|
151
|
+
"--exec-path",
|
|
152
|
+
]),
|
|
153
|
+
denyFlags: Object.freeze([
|
|
154
|
+
"-C",
|
|
155
|
+
"-c",
|
|
156
|
+
"--config-env",
|
|
157
|
+
"--git-dir",
|
|
158
|
+
"--work-tree",
|
|
159
|
+
"--namespace",
|
|
160
|
+
"--exec-path",
|
|
161
|
+
"--ext-diff",
|
|
162
|
+
"--textconv",
|
|
163
|
+
"--no-index",
|
|
164
|
+
"--contents",
|
|
165
|
+
"--output",
|
|
166
|
+
// Force / history-rewrite flags are denied at the boundary as well as refused by the builder.
|
|
167
|
+
"--force",
|
|
168
|
+
"-f",
|
|
169
|
+
"--force-with-lease",
|
|
170
|
+
"--force-if-includes",
|
|
171
|
+
"--mirror",
|
|
172
|
+
"--delete",
|
|
173
|
+
"-d",
|
|
174
|
+
]),
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
export class GitPublishArgvError extends Error {
|
|
178
|
+
constructor(message) {
|
|
179
|
+
super(message);
|
|
180
|
+
this.name = "GitPublishArgvError";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// NUL and the rest of the C0 control range plus DEL. A git ref never legitimately contains one;
|
|
184
|
+
// rejecting them is defence-in-depth above git's own ref validation. NUL is NOT matched by \s, so
|
|
185
|
+
// the control range is enumerated via a string-built RegExp (no literal control chars in source).
|
|
186
|
+
// eslint-disable-next-line no-control-regex -- intentionally matches control chars to REJECT them
|
|
187
|
+
const REF_CONTROL_CHAR = new RegExp("[\u0000-\u001f\u007f]");
|
|
188
|
+
function assertRef(value, label) {
|
|
189
|
+
if (value.length === 0) {
|
|
190
|
+
throw new GitPublishArgvError(`${label} must not be empty`);
|
|
191
|
+
}
|
|
192
|
+
if (REF_CONTROL_CHAR.test(value)) {
|
|
193
|
+
throw new GitPublishArgvError(`${label} must not contain control characters`);
|
|
194
|
+
}
|
|
195
|
+
if (/\s/.test(value)) {
|
|
196
|
+
throw new GitPublishArgvError(`${label} must not contain whitespace`);
|
|
197
|
+
}
|
|
198
|
+
if (value.startsWith("-")) {
|
|
199
|
+
throw new GitPublishArgvError(`${label} must not start with "-" (flag-injection guard)`);
|
|
200
|
+
}
|
|
201
|
+
if (value.includes(":")) {
|
|
202
|
+
throw new GitPublishArgvError(`${label} must not contain ":" (refspec-injection guard)`);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
// Builds the single governed push argv. An explicit `src:dst` refspec is always used so the push target
|
|
207
|
+
// is never inferred from ambient `push.default` config. AC4: a force push is REFUSED here — there is no
|
|
208
|
+
// branch in this builder that can emit a force flag.
|
|
209
|
+
export function buildPushArgv(command) {
|
|
210
|
+
if (command.forcePush) {
|
|
211
|
+
throw new GitPublishArgvError("force push is not permitted (AC4 — blocked by default)");
|
|
212
|
+
}
|
|
213
|
+
const remote = assertRef(command.remoteAlias, "remoteAlias");
|
|
214
|
+
const source = assertRef(command.sourceBranchName, "sourceBranchName");
|
|
215
|
+
const target = assertRef(command.remoteBranchName, "remoteBranchName");
|
|
216
|
+
const argv = ["push"];
|
|
217
|
+
if (command.setUpstreamTracking) {
|
|
218
|
+
argv.push("--set-upstream");
|
|
219
|
+
}
|
|
220
|
+
argv.push(remote, `${source}:${target}`);
|
|
221
|
+
return argv;
|
|
222
|
+
}
|
|
223
|
+
function pushResolvedInputs(command) {
|
|
224
|
+
return {
|
|
225
|
+
kind: "push",
|
|
226
|
+
sourceBranchName: command.sourceBranchName,
|
|
227
|
+
remoteAlias: command.remoteAlias,
|
|
228
|
+
remoteBranchName: command.remoteBranchName,
|
|
229
|
+
forcePush: command.forcePush,
|
|
230
|
+
setUpstreamTracking: command.setUpstreamTracking,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// The remote target a branch-pattern policy constraint is evaluated against: the published-to branch,
|
|
234
|
+
// so a protected/shared REMOTE target is judged by policy, not the local source name.
|
|
235
|
+
function buildPushPreview(command, snapshot) {
|
|
236
|
+
return {
|
|
237
|
+
schemaVersion: GIT_DELIVERY_SCHEMA_VERSION,
|
|
238
|
+
affectedBranchName: command.remoteBranchName,
|
|
239
|
+
wouldCreateRemoteBranch: command.setUpstreamTracking && !snapshot.hasUpstream,
|
|
240
|
+
wouldTriggerChecks: true,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function assemblePushEnvelope(actionId, inputs, policyDecision, approval, preview, executionResult) {
|
|
244
|
+
return {
|
|
245
|
+
schemaVersion: GIT_DELIVERY_SCHEMA_VERSION,
|
|
246
|
+
actionId,
|
|
247
|
+
kind: "push",
|
|
248
|
+
resolvedInputs: inputs,
|
|
249
|
+
policyDecision,
|
|
250
|
+
approvalRequirement: approval,
|
|
251
|
+
preview,
|
|
252
|
+
...(executionResult !== undefined ? { executionResult } : {}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function approvalState(approval, now) {
|
|
256
|
+
if (!approval.required) {
|
|
257
|
+
return "absent";
|
|
258
|
+
}
|
|
259
|
+
if (approval.expiresAtMs !== undefined && approval.expiresAtMs <= now) {
|
|
260
|
+
return "expired";
|
|
261
|
+
}
|
|
262
|
+
return "valid";
|
|
263
|
+
}
|
|
264
|
+
function constraintBlock(constraint, target, capabilities, pushInputs) {
|
|
265
|
+
if (constraint.kind === "branch-pattern") {
|
|
266
|
+
const ok = target !== undefined && gitDeliveryBranchNameMatchesAny(target, constraint.patterns);
|
|
267
|
+
return ok ? undefined : "policy-pack-blocked";
|
|
268
|
+
}
|
|
269
|
+
if (constraint.kind === "provider-capability") {
|
|
270
|
+
return capabilities.includes(constraint.capability) ? undefined : "provider-capability-absent";
|
|
271
|
+
}
|
|
272
|
+
// The FORCE-AWARE risk class: a force push escalates to recovery-or-rewrite (severity 4), so the
|
|
273
|
+
// publish ceiling (severity 2) blocks it (AC4). A future pack with a recovery-or-rewrite ceiling is
|
|
274
|
+
// the "explicit policy path" that could permit force; the default publish ceiling does not.
|
|
275
|
+
const pushSeverity = GIT_DELIVERY_RISK_CLASS_SEVERITY[gitDeliveryRiskClassForInputs(pushInputs)];
|
|
276
|
+
const ceilingSeverity = GIT_DELIVERY_RISK_CLASS_SEVERITY[constraint.maxRiskClass];
|
|
277
|
+
return pushSeverity <= ceilingSeverity ? undefined : "risk-class-ceiling";
|
|
278
|
+
}
|
|
279
|
+
export function evaluateGitPublishEffectivePolicy(decision, target, capabilities, pushInputs) {
|
|
280
|
+
if (decision.outcome === "allowed") {
|
|
281
|
+
return { outcome: "allowed" };
|
|
282
|
+
}
|
|
283
|
+
if (decision.outcome === "blocked") {
|
|
284
|
+
return { outcome: "blocked", blockReason: decision.reason };
|
|
285
|
+
}
|
|
286
|
+
if (decision.outcome === "approval-gated") {
|
|
287
|
+
return { outcome: "approval-gated" };
|
|
288
|
+
}
|
|
289
|
+
for (const constraint of decision.constraints) {
|
|
290
|
+
const reason = constraintBlock(constraint, target, capabilities, pushInputs);
|
|
291
|
+
if (reason !== undefined) {
|
|
292
|
+
return { outcome: "blocked", blockReason: reason };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return { outcome: "allowed" };
|
|
296
|
+
}
|
|
297
|
+
function resolvePublishGate(decision, approval, target, capabilities, pushInputs, now) {
|
|
298
|
+
if (decision.outcome === "allowed") {
|
|
299
|
+
return { proceed: true };
|
|
300
|
+
}
|
|
301
|
+
if (decision.outcome === "blocked") {
|
|
302
|
+
return { proceed: false, status: "policy-block", reason: decision.reason };
|
|
303
|
+
}
|
|
304
|
+
if (decision.outcome === "approval-gated") {
|
|
305
|
+
const state = approvalState(approval, now);
|
|
306
|
+
if (state === "valid")
|
|
307
|
+
return { proceed: true };
|
|
308
|
+
if (state === "expired") {
|
|
309
|
+
return { proceed: false, status: "policy-block", reason: "approval-expired" };
|
|
310
|
+
}
|
|
311
|
+
return { proceed: false, status: "approval-required", approvers: decision.requiredApprovers };
|
|
312
|
+
}
|
|
313
|
+
for (const constraint of decision.constraints) {
|
|
314
|
+
const reason = constraintBlock(constraint, target, capabilities, pushInputs);
|
|
315
|
+
if (reason !== undefined) {
|
|
316
|
+
return { proceed: false, status: "policy-block", reason };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { proceed: true };
|
|
320
|
+
}
|
|
321
|
+
// ─── Execution outcome mapping (reuses the taxonomy) ─────────────────────────────────────────
|
|
322
|
+
function publishOutcomeFor(result) {
|
|
323
|
+
if (result.outcome === "succeeded") {
|
|
324
|
+
return { status: "succeeded", executionResult: result };
|
|
325
|
+
}
|
|
326
|
+
const category = gitMutationCategoryForExecutionResult(result) ?? "execution-failure";
|
|
327
|
+
if (category === "recovery-required") {
|
|
328
|
+
return { status: "recovery-required", category, executionResult: result };
|
|
329
|
+
}
|
|
330
|
+
if (category === "provider-failure") {
|
|
331
|
+
return { status: "failed", category, executionResult: result };
|
|
332
|
+
}
|
|
333
|
+
return { status: "failed", category: "execution-failure", executionResult: result };
|
|
334
|
+
}
|
|
335
|
+
function preparePublish(request, deps) {
|
|
336
|
+
const inputs = pushResolvedInputs(request.command);
|
|
337
|
+
const capabilities = deps.activeProviderCapabilities ?? [];
|
|
338
|
+
const context = {
|
|
339
|
+
actionKind: "push",
|
|
340
|
+
targetBranchName: request.command.remoteBranchName,
|
|
341
|
+
activeProviderCapabilities: capabilities,
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
inputs,
|
|
345
|
+
preflight: evaluateGitPreflight(inputs, deps.snapshot),
|
|
346
|
+
preview: buildPushPreview(request.command, deps.snapshot),
|
|
347
|
+
policyDecision: evaluateGitPolicy(deps.orgPolicyPack, deps.repoPolicyPack, context),
|
|
348
|
+
actionId: deps.newActionId(),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function lifecycleFor(prep, approval, outcome, phaseReached, executionResult) {
|
|
352
|
+
return {
|
|
353
|
+
envelope: assemblePushEnvelope(prep.actionId, prep.inputs, prep.policyDecision, approval, prep.preview, executionResult),
|
|
354
|
+
outcome,
|
|
355
|
+
phaseReached,
|
|
356
|
+
preflight: prep.preflight,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
// Isolates the adapter call so a thrown/rejected adapter becomes a structured internal-error result
|
|
360
|
+
// (an argv-builder refusal of a force push lands here too) rather than escaping the gateway.
|
|
361
|
+
async function runPublishAdapter(command, adapter) {
|
|
362
|
+
try {
|
|
363
|
+
// Build the argv eagerly so a force-push refusal (AC4) is a structured failure, never a spawn.
|
|
364
|
+
buildPushArgv(command);
|
|
365
|
+
return await adapter.publish({
|
|
366
|
+
sourceBranchName: command.sourceBranchName,
|
|
367
|
+
remoteAlias: command.remoteAlias,
|
|
368
|
+
remoteBranchName: command.remoteBranchName,
|
|
369
|
+
setUpstreamTracking: command.setUpstreamTracking,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return {
|
|
374
|
+
schemaVersion: GIT_DELIVERY_SCHEMA_VERSION,
|
|
375
|
+
outcome: "failed",
|
|
376
|
+
durationMs: 0,
|
|
377
|
+
errorCode: "internal-error",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Runs ONE governed publish end-to-end: derive push inputs → preflight (push case) → preview → policy →
|
|
383
|
+
* (only when preflight passes, policy permits, approval satisfied) execute through the narrow remote
|
|
384
|
+
* adapter. Returns a kernel-shaped lifecycle result (so the #474 evidence builder records it unchanged)
|
|
385
|
+
* plus the live publish-rejection descriptor when the remote rejected an executed push.
|
|
386
|
+
*/
|
|
387
|
+
export async function runGitPublish(request, deps) {
|
|
388
|
+
const prep = preparePublish(request, deps);
|
|
389
|
+
// Gate 1: a blocking preflight finding halts before policy and execution.
|
|
390
|
+
if (!prep.preflight.ok) {
|
|
391
|
+
const outcome = {
|
|
392
|
+
status: "blocked",
|
|
393
|
+
category: "preflight-block",
|
|
394
|
+
findings: prep.preflight.blocking,
|
|
395
|
+
};
|
|
396
|
+
return { lifecycle: lifecycleFor(prep, request.approval, outcome, "preflight", undefined) };
|
|
397
|
+
}
|
|
398
|
+
// Gate 2: policy must permit (and any required approval must be satisfied).
|
|
399
|
+
const gate = resolvePublishGate(prep.policyDecision, request.approval, request.command.remoteBranchName, deps.activeProviderCapabilities ?? [], prep.inputs, deps.now());
|
|
400
|
+
if (!gate.proceed) {
|
|
401
|
+
const outcome = gate.status === "approval-required"
|
|
402
|
+
? { status: "approval-required", requiredApprovers: gate.approvers }
|
|
403
|
+
: { status: "blocked", category: "policy-block", blockReason: gate.reason };
|
|
404
|
+
return { lifecycle: lifecycleFor(prep, request.approval, outcome, "policy", undefined) };
|
|
405
|
+
}
|
|
406
|
+
// Execute through the narrow remote adapter and classify any rejection.
|
|
407
|
+
const result = await runPublishAdapter(request.command, deps.adapter);
|
|
408
|
+
const outcome = publishOutcomeFor(result);
|
|
409
|
+
const lifecycle = lifecycleFor(prep, request.approval, outcome, "result", result);
|
|
410
|
+
// Attach a publish-rejection descriptor only for a genuine remote/precondition rejection. An ABORTED
|
|
411
|
+
// outcome (the run was cancelled) is not a remote rejection, so it carries no "user-fixable" recovery
|
|
412
|
+
// hint — surfacing one would mislead the operator into thinking the remote refused the push.
|
|
413
|
+
if (result.outcome !== "succeeded" && result.outcome !== "aborted") {
|
|
414
|
+
const reason = result.rejectionReason ?? "unknown";
|
|
415
|
+
return { lifecycle, rejection: gitPublishRejectionFor(reason) };
|
|
416
|
+
}
|
|
417
|
+
return { lifecycle };
|
|
418
|
+
}
|
|
419
|
+
// True iff the argv begins with the single allowed publish subcommand. Lets tests prove the
|
|
420
|
+
// no-generic-fallback property structurally for the remote authority.
|
|
421
|
+
export function gitPublishArgvIsGoverned(argv) {
|
|
422
|
+
return argv.length > 0 && GIT_PUBLISH_ALLOWED_SUBCOMMANDS.includes(argv[0] ?? "");
|
|
423
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WorkspaceInfo } from "@oscharko-dev/keiko-workspace";
|
|
2
|
+
import { type GitRemotePublishAdapter } from "./git-publish-gateway.js";
|
|
3
|
+
import { type ExecutableResolver, type HomeProvider, type SpawnFn } from "./exec.js";
|
|
4
|
+
import { type SandboxPolicy } from "./types.js";
|
|
5
|
+
export interface NodeGitPublishAdapterDeps {
|
|
6
|
+
readonly workspace: WorkspaceInfo;
|
|
7
|
+
readonly processEnv?: NodeJS.ProcessEnv | undefined;
|
|
8
|
+
readonly now?: (() => number) | undefined;
|
|
9
|
+
readonly spawn?: SpawnFn | undefined;
|
|
10
|
+
readonly policy?: SandboxPolicy | undefined;
|
|
11
|
+
readonly resolveExecutable?: ExecutableResolver | undefined;
|
|
12
|
+
readonly home?: HomeProvider | undefined;
|
|
13
|
+
readonly signal?: AbortSignal | undefined;
|
|
14
|
+
readonly timeoutMs?: number | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function createNodeGitPublishAdapter(deps: NodeGitPublishAdapterDeps): GitRemotePublishAdapter;
|
|
17
|
+
//# sourceMappingURL=git-publish-node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-publish-node.d.ts","sourceRoot":"","sources":["../src/git-publish-node.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAKnE,OAAO,EAOL,KAAK,uBAAuB,EAC7B,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,YAAY,EAEjB,KAAK,OAAO,EACb,MAAM,WAAW,CAAC;AACnB,OAAO,EAA8C,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5F,MAAM,WAAW,yBAAyB;IAExC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,UAAU,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAGrC,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAwFD,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,yBAAyB,GAC9B,uBAAuB,CAezB"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Node implementation of the narrow remote publish adapter (Issue #476, Epic #470) — AC3/AC5.
|
|
2
|
+
//
|
|
3
|
+
// This is the ONLY place a governed `git push` actually executes. It builds the single governed push
|
|
4
|
+
// argv from the pure builder (git-publish-gateway.ts) and runs it through the SAME keiko-tools no-shell
|
|
5
|
+
// spawn boundary (`runCommand`, exec.ts) with a DEDICATED publish allowlist (`GIT_PUBLISH_COMMAND_RULES`)
|
|
6
|
+
// that permits only `push` and denies every force/history-rewrite flag. There is no method that accepts
|
|
7
|
+
// an arbitrary command string and no parallel child_process path: the deny-by-default allowlist, env
|
|
8
|
+
// isolation, redaction, and cancellation of the shared boundary apply to the publish exactly as to every
|
|
9
|
+
// other tool.
|
|
10
|
+
//
|
|
11
|
+
// A non-zero push exit is classified into a typed GitPublishRejectionReason by matching git's own
|
|
12
|
+
// English status phrases in the (already secret-redacted) output. Raw stdout/stderr never leave this
|
|
13
|
+
// module — only the typed reason and the content-free contract error code cross the boundary.
|
|
14
|
+
//
|
|
15
|
+
// Lives on the `./internal/git-mutation` subpath (re-exported by git-mutation-node.ts) because it carries
|
|
16
|
+
// the Node execution effect; the pure port, builder, and rules it implements are on the package barrel.
|
|
17
|
+
import { GIT_DELIVERY_SCHEMA_VERSION, } from "@oscharko-dev/keiko-contracts";
|
|
18
|
+
import { buildPushArgv, classifyGitPublishRejection, GIT_PUBLISH_COMMAND_RULES, gitPublishRejectionToErrorCode, } from "./git-publish-gateway.js";
|
|
19
|
+
import { CommandCancelledError, CommandTimeoutError } from "./errors.js";
|
|
20
|
+
import { nodeSpawnFn, runCommand, } from "./exec.js";
|
|
21
|
+
import { DEFAULT_SANDBOX_POLICY } from "./types.js";
|
|
22
|
+
function executionResult(outcome, durationMs, extra) {
|
|
23
|
+
return {
|
|
24
|
+
schemaVersion: GIT_DELIVERY_SCHEMA_VERSION,
|
|
25
|
+
outcome,
|
|
26
|
+
durationMs: Math.max(0, Math.trunc(durationMs)),
|
|
27
|
+
...extra,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function buildRunContext(deps) {
|
|
31
|
+
return {
|
|
32
|
+
runDeps: {
|
|
33
|
+
workspace: deps.workspace,
|
|
34
|
+
policy: deps.policy ?? DEFAULT_SANDBOX_POLICY,
|
|
35
|
+
commandRules: GIT_PUBLISH_COMMAND_RULES,
|
|
36
|
+
spawn: deps.spawn ?? nodeSpawnFn,
|
|
37
|
+
processEnv: deps.processEnv ?? process.env,
|
|
38
|
+
now: deps.now ?? Date.now,
|
|
39
|
+
...(deps.resolveExecutable !== undefined
|
|
40
|
+
? { resolveExecutable: deps.resolveExecutable }
|
|
41
|
+
: {}),
|
|
42
|
+
...(deps.home !== undefined ? { home: deps.home } : {}),
|
|
43
|
+
},
|
|
44
|
+
signal: deps.signal ?? new AbortController().signal,
|
|
45
|
+
timeoutMs: deps.timeoutMs,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// A non-zero push exit means the remote (or a local precondition) rejected the push. The reason is
|
|
49
|
+
// classified from the combined, already-redacted output; only the typed reason + content-free error
|
|
50
|
+
// code are returned. A `timedOut` run is a transient network timeout, not a remote rejection.
|
|
51
|
+
function rejectionFromExit(result) {
|
|
52
|
+
if (result.timedOut) {
|
|
53
|
+
return executionResult("failed", result.durationMs, {
|
|
54
|
+
errorCode: "timeout",
|
|
55
|
+
rejectionReason: "remote-unavailable",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const reason = classifyGitPublishRejection(`${result.stdout}\n${result.stderr}`);
|
|
59
|
+
return executionResult("failed", result.durationMs, {
|
|
60
|
+
errorCode: gitPublishRejectionToErrorCode(reason),
|
|
61
|
+
rejectionReason: reason,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function failureFromThrow(error, durationMs) {
|
|
65
|
+
if (error instanceof CommandTimeoutError) {
|
|
66
|
+
return executionResult("failed", durationMs, {
|
|
67
|
+
errorCode: "timeout",
|
|
68
|
+
rejectionReason: "remote-unavailable",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (error instanceof CommandCancelledError) {
|
|
72
|
+
return executionResult("aborted", durationMs);
|
|
73
|
+
}
|
|
74
|
+
// A denied command (our own argv hit the allowlist), an argv-construction fault, or any other throw
|
|
75
|
+
// is an internal gateway error — it never means the remote is at fault.
|
|
76
|
+
return executionResult("failed", durationMs, { errorCode: "internal-error" });
|
|
77
|
+
}
|
|
78
|
+
async function runPush(ctx, argv) {
|
|
79
|
+
let result;
|
|
80
|
+
try {
|
|
81
|
+
result = await runCommand({ command: "git", args: argv, cwd: undefined, timeoutMs: ctx.timeoutMs, signal: ctx.signal }, ctx.runDeps);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return failureFromThrow(error, 0);
|
|
85
|
+
}
|
|
86
|
+
if (result.exitCode === 0) {
|
|
87
|
+
return executionResult("succeeded", result.durationMs);
|
|
88
|
+
}
|
|
89
|
+
return rejectionFromExit(result);
|
|
90
|
+
}
|
|
91
|
+
export function createNodeGitPublishAdapter(deps) {
|
|
92
|
+
const ctx = buildRunContext(deps);
|
|
93
|
+
return {
|
|
94
|
+
publish: (req) => {
|
|
95
|
+
let argv;
|
|
96
|
+
try {
|
|
97
|
+
// forcePush is hard-false and LAST so it can never be overridden by the request: a force
|
|
98
|
+
// operand never reaches this executor (AC4 defence-in-depth above the gateway gate).
|
|
99
|
+
argv = buildPushArgv({ ...req, kind: "push", forcePush: false });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return Promise.resolve(executionResult("failed", 0, { errorCode: "internal-error" }));
|
|
103
|
+
}
|
|
104
|
+
return runPush(ctx, argv);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|