@remogram/core 0.1.0-beta.1 → 0.1.0-beta.10
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/auth-classes.js +73 -0
- package/branch-protection.js +155 -0
- package/caps.js +162 -14
- package/change-request-merge-execute.js +148 -0
- package/check-diagnostics.js +92 -0
- package/check-pagination.js +216 -0
- package/config-schema.js +21 -0
- package/contracts/envelope.js +26 -2
- package/contracts/errors.js +14 -2
- package/contracts/forge-error-fields.js +124 -0
- package/contracts/observer-fact-inventory.js +64 -0
- package/contracts/semantic-diff-facts.js +168 -0
- package/cr-comments.js +93 -0
- package/cr-files.js +62 -0
- package/cr-inventory-cursor.js +64 -0
- package/cr-inventory.js +136 -0
- package/cr-open.js +38 -0
- package/effective-write-policy.js +68 -0
- package/forge-changes-cursor.js +88 -0
- package/forge-changes.js +181 -0
- package/forge-identity.js +42 -0
- package/git-args.js +19 -0
- package/git-local.js +20 -0
- package/http.js +36 -2
- package/idempotency.js +69 -0
- package/index.js +255 -3
- package/issue-open.js +50 -0
- package/merge-blockers.js +68 -0
- package/merge-plan-forge.js +63 -0
- package/merge-plan.js +82 -0
- package/merge-policy.js +55 -0
- package/open-pull-list.js +256 -0
- package/operator-config.js +260 -0
- package/package.json +1 -1
- package/path-allowlist.js +114 -0
- package/pr-head-reconcile.js +38 -0
- package/provider-health.js +93 -0
- package/ref-inventory.js +98 -0
- package/resolve.js +53 -4
- package/status-set.js +92 -0
- package/stub-provider.js +11 -8
- package/whoami.js +114 -0
- package/write-config.js +63 -0
- package/write-field-policy.js +93 -0
- package/write-readiness.js +91 -0
package/index.js
CHANGED
|
@@ -1,19 +1,254 @@
|
|
|
1
1
|
export { SCHEMA_VERSION, PACKET_TYPES, forgePacket, forgeErrorPacket, unknownForgeContext, FORBIDDEN_PACKET_KEYS } from './contracts/envelope.js';
|
|
2
|
+
export {
|
|
3
|
+
V1_READ_PLAN_COMMANDS,
|
|
4
|
+
FACT_INVENTORY_PACKET_TYPES,
|
|
5
|
+
TRUSTED_ENVELOPE_FIELDS,
|
|
6
|
+
TRUSTED_NORMALIZED_BODY_FIELDS,
|
|
7
|
+
FORGE_SOURCED_STRING_LEAVES,
|
|
8
|
+
FACT_INVENTORY_BODY_SHAPES,
|
|
9
|
+
forgeFactInventoryPacket,
|
|
10
|
+
} from './contracts/semantic-diff-facts.js';
|
|
11
|
+
export {
|
|
12
|
+
OBSERVER_REMOGRAM_COMMANDS,
|
|
13
|
+
OBSERVER_FACT_INVENTORY_PACKETS,
|
|
14
|
+
observerProtoRemogramCommands,
|
|
15
|
+
semanticDiffFactCommands,
|
|
16
|
+
allObserverEligibleCommands,
|
|
17
|
+
} from './contracts/observer-fact-inventory.js';
|
|
2
18
|
export { ERROR_CODES, forgeError } from './contracts/errors.js';
|
|
19
|
+
export {
|
|
20
|
+
FORGE_ERROR_FIELD_ALLOWLIST,
|
|
21
|
+
normalizeForgeErrorFields,
|
|
22
|
+
} from './contracts/forge-error-fields.js';
|
|
3
23
|
export {
|
|
4
24
|
capText,
|
|
5
25
|
sanitizeField,
|
|
26
|
+
sanitizeReadField,
|
|
27
|
+
sanitizeWriteBody,
|
|
28
|
+
sanitizeWriteTitle,
|
|
6
29
|
sanitizeUrl,
|
|
7
30
|
readStreamCapped,
|
|
8
31
|
DEFAULT_MAX_BYTES,
|
|
9
32
|
DEFAULT_FIELD_MAX_BYTES,
|
|
10
33
|
FORGE_INGEST_MAX_BYTES_ENV,
|
|
34
|
+
MAX_FORGE_INGEST_ENV_BYTES,
|
|
35
|
+
DEFAULT_CHECK_STATUS_PAGE_SIZE,
|
|
36
|
+
MAX_CHECK_STATUS_PAGES,
|
|
37
|
+
DEFAULT_OPEN_PULL_LIST_PAGE_SIZE,
|
|
38
|
+
MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
11
39
|
getEffectiveIngestMaxBytes,
|
|
12
40
|
forgeIngestCapabilityFacts,
|
|
41
|
+
checkPaginationCapabilityFacts,
|
|
42
|
+
idempotencyScanCapabilityFacts,
|
|
43
|
+
statusSetIdempotencyScanCapabilityFacts,
|
|
44
|
+
openPullListCapabilityFacts,
|
|
13
45
|
} from './caps.js';
|
|
14
|
-
export {
|
|
15
|
-
|
|
46
|
+
export {
|
|
47
|
+
WRITE_FIELD_MAX_BYTES_ENV,
|
|
48
|
+
MAX_WRITE_FIELD_ENV_BYTES,
|
|
49
|
+
resolveEffectiveWriteFieldPolicy,
|
|
50
|
+
forgeWriteFieldCapabilityFacts,
|
|
51
|
+
getEffectiveWriteFieldMaxBytesFromEnv,
|
|
52
|
+
parseForgeWritePolicyBlock,
|
|
53
|
+
} from './write-field-policy.js';
|
|
54
|
+
export {
|
|
55
|
+
paginateCheckStatusPages,
|
|
56
|
+
paginateOffsetListPages,
|
|
57
|
+
fetchWithIngestPageBackoff,
|
|
58
|
+
fetchPageWithIngestBackoff,
|
|
59
|
+
withPerPageParam,
|
|
60
|
+
withLimitParam,
|
|
61
|
+
} from './check-pagination.js';
|
|
62
|
+
export { assertGitRef, assertGitRemote, assertCrOpenBranchRef } from './git-args.js';
|
|
63
|
+
export { gitRevParse, gitCurrentBranch, gitAheadBehind, gitRepoRoot, gitDiffNameOnly } from './git-local.js';
|
|
64
|
+
export { buildRefInventoryBody, refsInventory } from './ref-inventory.js';
|
|
65
|
+
export { buildCrInventoryEntry, buildHeadReconcile, crInventory, DEFAULT_CR_INVENTORY_LIMIT, DEFAULT_CR_INVENTORY_SAFE_LIMIT, normalizeCrInventoryLimit } from './cr-inventory.js';
|
|
66
|
+
export {
|
|
67
|
+
CR_INVENTORY_CURSOR_VERSION,
|
|
68
|
+
decodeCrInventoryCursor,
|
|
69
|
+
encodeCrInventoryCursor,
|
|
70
|
+
} from './cr-inventory-cursor.js';
|
|
71
|
+
export {
|
|
72
|
+
CR_INVENTORY_SLICE_SORTS,
|
|
73
|
+
DEFAULT_CR_INVENTORY_SLICE_SORT,
|
|
74
|
+
normalizeCrInventorySort,
|
|
75
|
+
parseTotalCountHeader,
|
|
76
|
+
isCrInventoryFastPathEligible,
|
|
77
|
+
forgeOrderAuthoritative,
|
|
78
|
+
validateFastPathPageLength,
|
|
79
|
+
isNumberSortFastPathEligible,
|
|
80
|
+
resolvePaginatedEntryCount,
|
|
81
|
+
resolveListTruncatedWithTrustedTotal,
|
|
82
|
+
isRecentCreatedFastPathEligible,
|
|
83
|
+
giteaRecentCreatedTailPage,
|
|
84
|
+
isNumberSortFullCollectRequired,
|
|
85
|
+
prepareGiteaOpenPullPageItems,
|
|
86
|
+
orderOpenPullNumbers,
|
|
87
|
+
buildOpenPullListMeta,
|
|
88
|
+
giteaOpenPullSortQuery,
|
|
89
|
+
gitlabOpenPullSortQuery,
|
|
90
|
+
githubOpenPullSortQuery,
|
|
91
|
+
appendSortQuery,
|
|
92
|
+
} from './open-pull-list.js';
|
|
93
|
+
export { buildChangeRequestOpenedBody } from './cr-open.js';
|
|
94
|
+
export { buildIssueOpenedBody, parseIssueOpenArgs } from './issue-open.js';
|
|
95
|
+
export {
|
|
96
|
+
assertExpectedSha,
|
|
97
|
+
mergeExecuteViewFacts,
|
|
98
|
+
mergeExecuteChecksFacts,
|
|
99
|
+
buildMergeExecuteBeforeFacts,
|
|
100
|
+
collectMergeExecuteBlockers,
|
|
101
|
+
buildCrMergeBlockedBody,
|
|
102
|
+
buildCrMergedBody,
|
|
103
|
+
buildMergeExecuteAfterFacts,
|
|
104
|
+
buildMergeExecuteMergeFacts,
|
|
105
|
+
} from './change-request-merge-execute.js';
|
|
106
|
+
export {
|
|
107
|
+
STATUS_SET_STATES,
|
|
108
|
+
assertCommitSha,
|
|
109
|
+
normalizeStatusSetState,
|
|
110
|
+
parseStatusSetArgs,
|
|
111
|
+
buildCommitStatusSetBody,
|
|
112
|
+
} from './status-set.js';
|
|
113
|
+
export {
|
|
114
|
+
buildProviderIdentityBody,
|
|
115
|
+
buildProviderIdentityFromGiteaUser,
|
|
116
|
+
parseGitHubOAuthScopes,
|
|
117
|
+
githubCanWriteFromScopes,
|
|
118
|
+
buildProviderIdentityFromGitHubUser,
|
|
119
|
+
normalizeGitLabCanWrite,
|
|
120
|
+
parseGitLabPatSelfSignals,
|
|
121
|
+
buildProviderIdentityFromGitLabUser,
|
|
122
|
+
normalizeGiteaCanWrite,
|
|
123
|
+
unimplementedTokenScopeSignal,
|
|
124
|
+
unimplementedTokenExpirySignal,
|
|
125
|
+
} from './whoami.js';
|
|
126
|
+
export {
|
|
127
|
+
buildBranchProtectionBody,
|
|
128
|
+
buildBranchProtectionFromGiteaProtection,
|
|
129
|
+
buildBranchProtectionFromGitHubProtection,
|
|
130
|
+
buildBranchProtectionFromGitLabProtection,
|
|
131
|
+
unimplementedApprovalsRequiredSignal,
|
|
132
|
+
MAX_BRANCH_PROTECTION_STATUS_CONTEXTS,
|
|
133
|
+
MAX_BRANCH_PROTECTION_RULES,
|
|
134
|
+
} from './branch-protection.js';
|
|
135
|
+
export {
|
|
136
|
+
enrichCheckStatus,
|
|
137
|
+
buildCheckDiagnostics,
|
|
138
|
+
buildPrChecksBody,
|
|
139
|
+
} from './check-diagnostics.js';
|
|
140
|
+
export {
|
|
141
|
+
buildCrFilesBody,
|
|
142
|
+
buildCrFilesFromGiteaFiles,
|
|
143
|
+
buildCrFilesFromGitLabChanges,
|
|
144
|
+
MAX_CR_FILES_PATHS,
|
|
145
|
+
} from './cr-files.js';
|
|
146
|
+
export {
|
|
147
|
+
buildCrCommentsBody,
|
|
148
|
+
buildCrCommentsFromGiteaComments,
|
|
149
|
+
buildCrCommentsFromGitLabDiscussions,
|
|
150
|
+
normalizeCrComment,
|
|
151
|
+
MAX_CR_COMMENTS,
|
|
152
|
+
} from './cr-comments.js';
|
|
153
|
+
export {
|
|
154
|
+
parseSinceObservedAt,
|
|
155
|
+
buildForgeChangesBody,
|
|
156
|
+
buildForgeChangesFromGiteaPulls,
|
|
157
|
+
buildChecksConclusionObservedEvent,
|
|
158
|
+
appendForgeChangeEvents,
|
|
159
|
+
MAX_FORGE_CHANGES_EVENTS,
|
|
160
|
+
FORGE_CHANGE_EVENT_KINDS,
|
|
161
|
+
} from './forge-changes.js';
|
|
162
|
+
export {
|
|
163
|
+
encodeForgeChangesCursor,
|
|
164
|
+
decodeForgeChangesCursor,
|
|
165
|
+
paginateForgeChangesBody,
|
|
166
|
+
FORGE_CHANGES_CURSOR_VERSION,
|
|
167
|
+
DEFAULT_FORGE_CHANGES_PAGE_SIZE,
|
|
168
|
+
} from './forge-changes-cursor.js';
|
|
169
|
+
export {
|
|
170
|
+
WRITE_COMMAND_IDS,
|
|
171
|
+
CONFIGURED_WRITE_COMMANDS,
|
|
172
|
+
writeCommandSchema,
|
|
173
|
+
assertWriteCommandConfigured,
|
|
174
|
+
writeNotConfiguredMessage,
|
|
175
|
+
isWriteCommandConfigured,
|
|
176
|
+
buildOperatorConfigSnippet,
|
|
177
|
+
buildRepoConfigSnippet,
|
|
178
|
+
} from './write-config.js';
|
|
179
|
+
export {
|
|
180
|
+
resolveEffectiveWritePolicy,
|
|
181
|
+
isWriteCommandAllowed,
|
|
182
|
+
normalizeWritePolicyInput,
|
|
183
|
+
writeSourceForCommand,
|
|
184
|
+
} from './effective-write-policy.js';
|
|
185
|
+
export {
|
|
186
|
+
REMOGRAM_OPERATOR_CONFIG_ENV,
|
|
187
|
+
MAX_OPERATOR_CONFIG_BYTES,
|
|
188
|
+
operatorConfigSchema,
|
|
189
|
+
defaultOperatorConfigPath,
|
|
190
|
+
discoverOperatorConfigPath,
|
|
191
|
+
parseOperatorConfigFile,
|
|
192
|
+
loadOperatorConfigFile,
|
|
193
|
+
assertOperatorBindMatches,
|
|
194
|
+
loadOperatorConfig,
|
|
195
|
+
} from './operator-config.js';
|
|
196
|
+
export {
|
|
197
|
+
buildWriteReadiness,
|
|
198
|
+
writeReadinessHasWarnings,
|
|
199
|
+
} from './write-readiness.js';
|
|
200
|
+
export {
|
|
201
|
+
buildApiReachabilityCheck,
|
|
202
|
+
classifyReachabilityFailure,
|
|
203
|
+
LIVE_REACHABILITY_TIMEOUT_MS,
|
|
204
|
+
} from './provider-health.js';
|
|
205
|
+
export {
|
|
206
|
+
bindIdempotencyScope,
|
|
207
|
+
idempotencyFingerprint,
|
|
208
|
+
idempotencyPacketFields,
|
|
209
|
+
normalizeIdempotencyKey,
|
|
210
|
+
resetIdempotencyScopeBindings,
|
|
211
|
+
} from './idempotency.js';
|
|
212
|
+
export {
|
|
213
|
+
resolveMergePolicy,
|
|
214
|
+
parseTruthyEnv,
|
|
215
|
+
mergePolicyAuditFacts,
|
|
216
|
+
ALLOW_MISSING_CHECKS_ENV,
|
|
217
|
+
ALLOW_PENDING_CHECKS_ENV,
|
|
218
|
+
} from './merge-policy.js';
|
|
219
|
+
export { mergeBlockersFromFacts, isOpenPrState } from './merge-blockers.js';
|
|
220
|
+
export {
|
|
221
|
+
applyForgePathScopeForMergePlan,
|
|
222
|
+
isCrFilesScopeComplete,
|
|
223
|
+
resolveMergePlanPathScope,
|
|
224
|
+
buildMergePlanBody,
|
|
225
|
+
buildMergePlanBodyFromFacts,
|
|
226
|
+
normalizeAllowedPaths,
|
|
227
|
+
} from './merge-plan.js';
|
|
228
|
+
export {
|
|
229
|
+
isMergePlanForgeScopeRethrowError,
|
|
230
|
+
MERGE_PLAN_FORGE_SCOPE_RETHROW_CODES,
|
|
231
|
+
resolveMergePlanOptsWithForgePaths,
|
|
232
|
+
buildMergePlanFromProviderFacts,
|
|
233
|
+
} from './merge-plan-forge.js';
|
|
234
|
+
export {
|
|
235
|
+
matchPathAllowlist,
|
|
236
|
+
isPathAllowed,
|
|
237
|
+
pathsOutsideAllowlist,
|
|
238
|
+
allPathsAllowed,
|
|
239
|
+
normalizeRepoRelativePath,
|
|
240
|
+
normalizeChangedPathList,
|
|
241
|
+
} from './path-allowlist.js';
|
|
242
|
+
export {
|
|
243
|
+
localHeadShaForPr,
|
|
244
|
+
staleHeadDetails,
|
|
245
|
+
staleHeadForgeError,
|
|
246
|
+
staleHeadForgeError as staleHeadError,
|
|
247
|
+
STALE_HEAD_MESSAGE,
|
|
248
|
+
throwIfStaleHeadByNumber,
|
|
249
|
+
} from './pr-head-reconcile.js';
|
|
16
250
|
export { parseConfigFile, configSchema } from './config-schema.js';
|
|
251
|
+
export { normalizedForgeOrigin } from './forge-identity.js';
|
|
17
252
|
export {
|
|
18
253
|
findConfigPath,
|
|
19
254
|
loadConfig,
|
|
@@ -23,5 +258,22 @@ export {
|
|
|
23
258
|
assertConfigMatchesRemote,
|
|
24
259
|
assertForgeReady,
|
|
25
260
|
forgeContext,
|
|
261
|
+
resolveWritePolicyForForge,
|
|
262
|
+
prepareForgeContext,
|
|
26
263
|
} from './resolve.js';
|
|
27
|
-
export {
|
|
264
|
+
export {
|
|
265
|
+
fetchWithTimeout,
|
|
266
|
+
fetchJson,
|
|
267
|
+
fetchJsonWithMeta,
|
|
268
|
+
parseLinkHeader,
|
|
269
|
+
isTrustedPaginationUrl,
|
|
270
|
+
fetchTextCapped,
|
|
271
|
+
} from './http.js';
|
|
272
|
+
export {
|
|
273
|
+
AUTH_CLASS,
|
|
274
|
+
API_PROVIDER_COMMAND_AUTH,
|
|
275
|
+
commandCapability,
|
|
276
|
+
apiProviderCommands,
|
|
277
|
+
stubProviderCommands,
|
|
278
|
+
assertAuthClass,
|
|
279
|
+
} from './auth-classes.js';
|
package/issue-open.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { sanitizeReadField, sanitizeWriteBody, sanitizeWriteTitle, sanitizeUrl } from './caps.js';
|
|
2
|
+
import { ERROR_CODES, forgeError } from './contracts/errors.js';
|
|
3
|
+
|
|
4
|
+
/** Normalize Gitea issue create response into issue_opened body fields. */
|
|
5
|
+
export function buildIssueOpenedBody(
|
|
6
|
+
issue,
|
|
7
|
+
{ title },
|
|
8
|
+
{ reusedExisting = false, idempotencyFields = null } = {},
|
|
9
|
+
) {
|
|
10
|
+
const issueNumber = Number(issue?.number);
|
|
11
|
+
if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
|
|
12
|
+
throw Object.assign(new Error('Provider returned invalid issue number'), {
|
|
13
|
+
forgeError: forgeError(
|
|
14
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
15
|
+
'Provider returned invalid issue number',
|
|
16
|
+
),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const resolvedTitle = reusedExisting
|
|
20
|
+
? sanitizeReadField(issue?.title ?? title)
|
|
21
|
+
: sanitizeReadField(title ?? issue?.title);
|
|
22
|
+
const body = {
|
|
23
|
+
issue_number: issueNumber,
|
|
24
|
+
url: sanitizeUrl(issue.html_url ?? issue.url),
|
|
25
|
+
state: sanitizeReadField(String(issue?.state ?? 'open').toLowerCase()),
|
|
26
|
+
title: resolvedTitle,
|
|
27
|
+
};
|
|
28
|
+
if (reusedExisting) {
|
|
29
|
+
body.reused_existing = true;
|
|
30
|
+
} else {
|
|
31
|
+
body.created = true;
|
|
32
|
+
}
|
|
33
|
+
if (idempotencyFields && typeof idempotencyFields === 'object') {
|
|
34
|
+
Object.assign(body, idempotencyFields);
|
|
35
|
+
}
|
|
36
|
+
return body;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseIssueOpenArgs({ title, body: issueBody }, writeFieldPolicy = null) {
|
|
40
|
+
if (title == null || String(title).trim() === '') {
|
|
41
|
+
throw Object.assign(new Error('--title required'), {
|
|
42
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--title required for issue open'),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const parsed = { title: sanitizeWriteTitle(String(title), writeFieldPolicy) };
|
|
46
|
+
if (issueBody != null && String(issueBody).trim() !== '') {
|
|
47
|
+
parsed.body = sanitizeWriteBody(String(issueBody), writeFieldPolicy);
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { allPathsAllowed, normalizeChangedPathList } from './path-allowlist.js';
|
|
2
|
+
|
|
3
|
+
function uniqueSorted(values) {
|
|
4
|
+
return [...new Set(values.filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function diagnosticBlockers(checks) {
|
|
8
|
+
const blockers = [];
|
|
9
|
+
const missing = checks.missing_required_contexts;
|
|
10
|
+
if (Array.isArray(missing) && missing.length > 0) blockers.push('required_checks_missing');
|
|
11
|
+
const stale = checks.stale_contexts;
|
|
12
|
+
if (Array.isArray(stale) && stale.length > 0) blockers.push('stale_status_context');
|
|
13
|
+
const required = new Set(
|
|
14
|
+
Array.isArray(checks.required_contexts) ? checks.required_contexts.filter(Boolean) : [],
|
|
15
|
+
);
|
|
16
|
+
if (required.size > 0 && Array.isArray(checks.pending_contexts)) {
|
|
17
|
+
const pendingRequired = checks.pending_contexts.filter((context) => required.has(context));
|
|
18
|
+
if (pendingRequired.length > 0) blockers.push('required_checks_pending');
|
|
19
|
+
}
|
|
20
|
+
return uniqueSorted(blockers);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Derive merge blockers from already-fetched PR view and checks facts.
|
|
25
|
+
* Shared by merge plan and cr inventory aggregation.
|
|
26
|
+
*/
|
|
27
|
+
export function isOpenPrState(state) {
|
|
28
|
+
return String(state ?? '').toLowerCase() === 'open';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} view
|
|
33
|
+
* @param {object} checks
|
|
34
|
+
* @param {{ allowed_paths?: string[], changed_paths?: string[] | null }} [pathScope]
|
|
35
|
+
*/
|
|
36
|
+
export function mergeBlockersFromFacts(view, checks, pathScope = {}, policy = {}) {
|
|
37
|
+
const blockers = [];
|
|
38
|
+
if (view.mergeability === 'conflicted') blockers.push('merge_conflict');
|
|
39
|
+
if (!isOpenPrState(view.state)) blockers.push('pr_not_open');
|
|
40
|
+
if (checks.checks_truncated === true) blockers.push('checks_incomplete');
|
|
41
|
+
if (checks.check_conclusion === 'failure') blockers.push('checks_failed');
|
|
42
|
+
if (checks.check_conclusion === 'missing' && !policy.allow_missing_checks) {
|
|
43
|
+
blockers.push('checks_missing');
|
|
44
|
+
}
|
|
45
|
+
if (checks.check_conclusion === 'pending' && !policy.allow_pending_checks) {
|
|
46
|
+
blockers.push('checks_pending');
|
|
47
|
+
}
|
|
48
|
+
blockers.push(...diagnosticBlockers(checks));
|
|
49
|
+
|
|
50
|
+
const allowedPaths = pathScope.allowed_paths;
|
|
51
|
+
if (Array.isArray(allowedPaths) && allowedPaths.length > 0) {
|
|
52
|
+
const changedPaths = pathScope.changed_paths;
|
|
53
|
+
if (changedPaths == null) {
|
|
54
|
+
blockers.push('changed_paths_unavailable');
|
|
55
|
+
} else if (Array.isArray(changedPaths)) {
|
|
56
|
+
const normalizedPaths = normalizeChangedPathList(changedPaths);
|
|
57
|
+
if (normalizedPaths == null) {
|
|
58
|
+
blockers.push('changed_paths_unavailable');
|
|
59
|
+
} else if (!allPathsAllowed(allowedPaths, normalizedPaths)) {
|
|
60
|
+
blockers.push('path_scope_violation');
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
blockers.push('changed_paths_unavailable');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return blockers;
|
|
68
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ERROR_CODES } from './contracts/errors.js';
|
|
2
|
+
import {
|
|
3
|
+
applyForgePathScopeForMergePlan,
|
|
4
|
+
buildMergePlanBodyFromFacts,
|
|
5
|
+
normalizeAllowedPaths,
|
|
6
|
+
} from './merge-plan.js';
|
|
7
|
+
|
|
8
|
+
/** Errors that must propagate from crFiles during merge-plan path scope — not mapped to changed_paths_unavailable. */
|
|
9
|
+
export const MERGE_PLAN_FORGE_SCOPE_RETHROW_CODES = new Set([
|
|
10
|
+
ERROR_CODES.UNAUTHENTICATED_PROVIDER,
|
|
11
|
+
ERROR_CODES.INVALID_ARGS,
|
|
12
|
+
ERROR_CODES.UNTRUSTED_BASE_URL,
|
|
13
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
14
|
+
ERROR_CODES.CONFIG_INVALID,
|
|
15
|
+
ERROR_CODES.OVERSIZED_RAW_OUTPUT,
|
|
16
|
+
ERROR_CODES.CONFIG_NOT_FOUND,
|
|
17
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
18
|
+
ERROR_CODES.STALE_HEAD,
|
|
19
|
+
ERROR_CODES.MISSING_REF,
|
|
20
|
+
ERROR_CODES.PR_NOT_OPEN,
|
|
21
|
+
ERROR_CODES.REMOTE_INFER_FAILED,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function isMergePlanForgeScopeRethrowError(err) {
|
|
25
|
+
const code = err?.forgeError?.code;
|
|
26
|
+
return typeof code === 'string' && MERGE_PLAN_FORGE_SCOPE_RETHROW_CODES.has(code);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { normalizeAllowedPaths } from './merge-plan.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve merge plan opts with forge cr_files when an allowlist is configured.
|
|
33
|
+
* @param {object} opts
|
|
34
|
+
* @param {() => Promise<object>} crFilesFn
|
|
35
|
+
*/
|
|
36
|
+
export async function resolveMergePlanOptsWithForgePaths(opts, crFilesFn) {
|
|
37
|
+
if (!normalizeAllowedPaths(opts.allowed_paths)) return opts;
|
|
38
|
+
try {
|
|
39
|
+
const crFilesBody = await crFilesFn();
|
|
40
|
+
return applyForgePathScopeForMergePlan(opts, crFilesBody);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (isMergePlanForgeScopeRethrowError(err)) throw err;
|
|
43
|
+
// Intentional mask bucket: transient api_error → changed_paths_unavailable.
|
|
44
|
+
// write_not_configured / idempotency codes are not emitted by crFiles today.
|
|
45
|
+
return applyForgePathScopeForMergePlan(opts, null);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build merge_plan body from provider deps (shared tri-provider orchestration).
|
|
51
|
+
* @param {object} ctx
|
|
52
|
+
* @param {object} opts
|
|
53
|
+
* @param {{ prView: Function, prChecks: Function, crFiles: Function }} deps
|
|
54
|
+
*/
|
|
55
|
+
export async function buildMergePlanFromProviderFacts(ctx, opts, deps) {
|
|
56
|
+
const view = await deps.prView(ctx, opts);
|
|
57
|
+
const checks = await deps.prChecks(ctx, { number: view.pr_number });
|
|
58
|
+
const mergeOpts = await resolveMergePlanOptsWithForgePaths(opts, () =>
|
|
59
|
+
deps.crFiles(ctx, { number: view.pr_number }),
|
|
60
|
+
);
|
|
61
|
+
const mergePolicy = opts.merge_policy ?? ctx.mergePolicy ?? {};
|
|
62
|
+
return buildMergePlanBodyFromFacts(view, checks, { ...mergeOpts, merge_policy: mergePolicy });
|
|
63
|
+
}
|
package/merge-plan.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mergeBlockersFromFacts } from './merge-blockers.js';
|
|
2
|
+
import { normalizeChangedPathList } from './path-allowlist.js';
|
|
3
|
+
|
|
4
|
+
function allowlistGlobHasDotDotSegment(glob) {
|
|
5
|
+
if (typeof glob !== 'string') return false;
|
|
6
|
+
const normalized = glob.replace(/\\/g, '/');
|
|
7
|
+
if (normalized === '..' || normalized.startsWith('../') || normalized.includes('/../')) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return normalized.split('/').some((segment) => segment === '..');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeAllowedPaths(allowedPaths) {
|
|
14
|
+
if (!Array.isArray(allowedPaths)) return null;
|
|
15
|
+
const normalized = allowedPaths
|
|
16
|
+
.filter((entry) => typeof entry === 'string')
|
|
17
|
+
.map((entry) => entry.trim())
|
|
18
|
+
.filter((entry) => entry.length > 0 && !allowlistGlobHasDotDotSegment(entry));
|
|
19
|
+
return normalized.length > 0 ? normalized : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** True when cr_files body is complete enough for merge-plan path scope. */
|
|
23
|
+
export function isCrFilesScopeComplete(crFilesBody) {
|
|
24
|
+
if (!crFilesBody || crFilesBody.paths_truncated) return false;
|
|
25
|
+
const changedPaths = crFilesBody.changed_paths;
|
|
26
|
+
if (!Array.isArray(changedPaths)) return false;
|
|
27
|
+
const pathCount = Number.isFinite(Number(crFilesBody.path_count))
|
|
28
|
+
? Math.floor(Number(crFilesBody.path_count))
|
|
29
|
+
: changedPaths.length;
|
|
30
|
+
if (pathCount > 0 && changedPaths.length === 0) return false;
|
|
31
|
+
if (pathCount > changedPaths.length) return false;
|
|
32
|
+
if (changedPaths.length > pathCount) return false;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Apply forge cr_files facts to merge plan opts when an allowlist is configured. */
|
|
37
|
+
export function applyForgePathScopeForMergePlan(opts, crFilesBody) {
|
|
38
|
+
if (!normalizeAllowedPaths(opts.allowed_paths)) return opts;
|
|
39
|
+
if (!isCrFilesScopeComplete(crFilesBody)) {
|
|
40
|
+
return { ...opts, changed_paths: null };
|
|
41
|
+
}
|
|
42
|
+
const normalizedPaths = normalizeChangedPathList(crFilesBody.changed_paths);
|
|
43
|
+
if (normalizedPaths == null) {
|
|
44
|
+
return { ...opts, changed_paths: null };
|
|
45
|
+
}
|
|
46
|
+
return { ...opts, changed_paths: normalizedPaths };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveMergePlanPathScope(opts = {}) {
|
|
50
|
+
const allowedPaths = normalizeAllowedPaths(opts.allowed_paths);
|
|
51
|
+
if (!allowedPaths) {
|
|
52
|
+
return { allowed_paths: null, changed_paths: null };
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(opts.changed_paths)) {
|
|
55
|
+
const normalizedPaths = normalizeChangedPathList(opts.changed_paths);
|
|
56
|
+
return {
|
|
57
|
+
allowed_paths: allowedPaths,
|
|
58
|
+
changed_paths: normalizedPaths,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { allowed_paths: allowedPaths, changed_paths: null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildMergePlanBody(view, checks, pathScope = {}, policy = {}) {
|
|
65
|
+
return {
|
|
66
|
+
pr_number: view.pr_number,
|
|
67
|
+
mergeability: view.mergeability,
|
|
68
|
+
checks_conclusion: checks.check_conclusion,
|
|
69
|
+
required_contexts: checks.required_contexts ?? [],
|
|
70
|
+
missing_required_contexts: checks.missing_required_contexts ?? [],
|
|
71
|
+
failed_contexts: checks.failed_contexts ?? [],
|
|
72
|
+
pending_contexts: checks.pending_contexts ?? [],
|
|
73
|
+
stale_contexts: checks.stale_contexts ?? [],
|
|
74
|
+
blockers: mergeBlockersFromFacts(view, checks, pathScope, policy),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function buildMergePlanBodyFromFacts(view, checks, opts = {}) {
|
|
79
|
+
const pathScope = resolveMergePlanPathScope(opts);
|
|
80
|
+
const policy = opts.merge_policy ?? {};
|
|
81
|
+
return buildMergePlanBody(view, checks, pathScope, policy);
|
|
82
|
+
}
|
package/merge-policy.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const ALLOW_MISSING_CHECKS_ENV = 'REMOGRAM_ALLOW_MISSING_CHECKS';
|
|
2
|
+
export const ALLOW_PENDING_CHECKS_ENV = 'REMOGRAM_ALLOW_PENDING_CHECKS';
|
|
3
|
+
|
|
4
|
+
/** @returns {boolean | null} true/false when recognized; null when unset/invalid */
|
|
5
|
+
export function parseTruthyEnv(value) {
|
|
6
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
7
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
|
8
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolvePolicyFlag(configValue, envName) {
|
|
13
|
+
const envRaw = process.env[envName];
|
|
14
|
+
if (envRaw != null && envRaw !== '') {
|
|
15
|
+
const parsed = parseTruthyEnv(envRaw);
|
|
16
|
+
if (parsed === true) {
|
|
17
|
+
return { value: true, source: 'env' };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (configValue === true) {
|
|
21
|
+
return { value: true, source: 'config' };
|
|
22
|
+
}
|
|
23
|
+
return { value: false, source: 'default' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolved merge policy for blocker derivation (forge-facts layer only).
|
|
28
|
+
* @param {object | null | undefined} config consumer .remogram.json
|
|
29
|
+
*/
|
|
30
|
+
export function resolveMergePolicy(config) {
|
|
31
|
+
const filePolicy = config?.merge_policy ?? {};
|
|
32
|
+
const missing = resolvePolicyFlag(filePolicy.allow_missing_checks, ALLOW_MISSING_CHECKS_ENV);
|
|
33
|
+
const pending = resolvePolicyFlag(filePolicy.allow_pending_checks, ALLOW_PENDING_CHECKS_ENV);
|
|
34
|
+
return {
|
|
35
|
+
allow_missing_checks: missing.value,
|
|
36
|
+
allow_pending_checks: pending.value,
|
|
37
|
+
source: {
|
|
38
|
+
allow_missing_checks: missing.source,
|
|
39
|
+
allow_pending_checks: pending.source,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Observational snapshot for merge execute before facts. */
|
|
45
|
+
export function mergePolicyAuditFacts(mergePolicy) {
|
|
46
|
+
if (!mergePolicy) return null;
|
|
47
|
+
return {
|
|
48
|
+
allow_missing_checks: mergePolicy.allow_missing_checks === true,
|
|
49
|
+
allow_pending_checks: mergePolicy.allow_pending_checks === true,
|
|
50
|
+
source: mergePolicy.source ?? {
|
|
51
|
+
allow_missing_checks: 'default',
|
|
52
|
+
allow_pending_checks: 'default',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|