@neat.is/core 0.7.9 → 0.7.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/dist/{chunk-N5TPODCX.js → chunk-6T7ZHODF.js} +486 -18
- package/dist/chunk-6T7ZHODF.js.map +1 -0
- package/dist/{chunk-ILG3SMD5.js → chunk-D4OX6MUX.js} +6 -2
- package/dist/chunk-D4OX6MUX.js.map +1 -0
- package/dist/cli.cjs +1256 -252
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +689 -164
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +505 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +505 -24
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +494 -21
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-ILG3SMD5.js.map +0 -1
- package/dist/chunk-N5TPODCX.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -2266,6 +2266,7 @@ var import_yaml = require("yaml");
|
|
|
2266
2266
|
var import_types3 = require("@neat.is/types");
|
|
2267
2267
|
var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
|
|
2268
2268
|
var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
|
|
2269
|
+
var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
|
|
2269
2270
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
2270
2271
|
"node_modules",
|
|
2271
2272
|
".git",
|
|
@@ -2305,6 +2306,7 @@ async function isPythonVenvDir(dir) {
|
|
|
2305
2306
|
function isConfigFile(name) {
|
|
2306
2307
|
const ext = import_node_path3.default.extname(name);
|
|
2307
2308
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2309
|
+
if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
|
|
2308
2310
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2309
2311
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
2310
2312
|
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
@@ -5409,6 +5411,21 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
5409
5411
|
await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(ctx.errorsPath), { recursive: true });
|
|
5410
5412
|
await import_node_fs7.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
5411
5413
|
}
|
|
5414
|
+
async function appendConnectorIncident(errorsPath, input) {
|
|
5415
|
+
const ev = {
|
|
5416
|
+
id: input.id,
|
|
5417
|
+
timestamp: input.timestamp,
|
|
5418
|
+
service: input.service,
|
|
5419
|
+
traceId: input.id,
|
|
5420
|
+
spanId: input.id,
|
|
5421
|
+
errorType: input.errorType,
|
|
5422
|
+
errorMessage: input.errorMessage,
|
|
5423
|
+
...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
|
|
5424
|
+
affectedNode: input.affectedNode
|
|
5425
|
+
};
|
|
5426
|
+
await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
|
|
5427
|
+
await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
5428
|
+
}
|
|
5412
5429
|
function incidentAffectedNode(span, graph, scanPath) {
|
|
5413
5430
|
const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
|
|
5414
5431
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
@@ -11222,8 +11239,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
|
|
|
11222
11239
|
var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
|
|
11223
11240
|
var import_types35 = require("@neat.is/types");
|
|
11224
11241
|
init_otel();
|
|
11225
|
-
var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
|
|
11226
11242
|
var PARSE_CHUNK10 = 16384;
|
|
11243
|
+
var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
|
|
11244
|
+
"Query",
|
|
11245
|
+
"QueryContext",
|
|
11246
|
+
"QueryRow",
|
|
11247
|
+
"QueryRowContext",
|
|
11248
|
+
"Exec",
|
|
11249
|
+
"ExecContext",
|
|
11250
|
+
"Prepare",
|
|
11251
|
+
"PrepareContext"
|
|
11252
|
+
]);
|
|
11253
|
+
var SQLX_METHODS = /* @__PURE__ */ new Set([
|
|
11254
|
+
"Get",
|
|
11255
|
+
"Select",
|
|
11256
|
+
"Queryx",
|
|
11257
|
+
"QueryRowx",
|
|
11258
|
+
"NamedExec",
|
|
11259
|
+
"NamedQuery",
|
|
11260
|
+
"MustExec",
|
|
11261
|
+
"Preparex",
|
|
11262
|
+
"GetContext",
|
|
11263
|
+
"SelectContext"
|
|
11264
|
+
]);
|
|
11265
|
+
var DATABASE_SQL_IMPORT = "database/sql";
|
|
11266
|
+
var SQLX_IMPORT = "github.com/jmoiron/sqlx";
|
|
11267
|
+
function makeGoParser3() {
|
|
11268
|
+
const p = new import_tree_sitter14.default();
|
|
11269
|
+
p.setLanguage(import_tree_sitter_go3.default);
|
|
11270
|
+
return p;
|
|
11271
|
+
}
|
|
11272
|
+
function parseSource10(parser, source) {
|
|
11273
|
+
return parser.parse(
|
|
11274
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
|
|
11275
|
+
);
|
|
11276
|
+
}
|
|
11227
11277
|
function walk7(node, visit) {
|
|
11228
11278
|
visit(node);
|
|
11229
11279
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -11231,25 +11281,54 @@ function walk7(node, visit) {
|
|
|
11231
11281
|
if (child) walk7(child, visit);
|
|
11232
11282
|
}
|
|
11233
11283
|
}
|
|
11284
|
+
function goStringLiteralValue(node) {
|
|
11285
|
+
if (!node) return null;
|
|
11286
|
+
if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
|
|
11287
|
+
const t = node.text;
|
|
11288
|
+
return t.length >= 2 ? t.slice(1, -1) : "";
|
|
11289
|
+
}
|
|
11290
|
+
return null;
|
|
11291
|
+
}
|
|
11292
|
+
function goImportsAny(root, names) {
|
|
11293
|
+
let found = false;
|
|
11294
|
+
walk7(root, (node) => {
|
|
11295
|
+
if (found || node.type !== "import_spec") return;
|
|
11296
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
11297
|
+
const value = goStringLiteralValue(node.namedChild(i));
|
|
11298
|
+
if (value !== null && names.has(value)) found = true;
|
|
11299
|
+
}
|
|
11300
|
+
});
|
|
11301
|
+
return found;
|
|
11302
|
+
}
|
|
11303
|
+
function firstStringLiteralArg(argsNode) {
|
|
11304
|
+
if (!argsNode) return null;
|
|
11305
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
11306
|
+
const value = goStringLiteralValue(argsNode.namedChild(i));
|
|
11307
|
+
if (value !== null) return value;
|
|
11308
|
+
}
|
|
11309
|
+
return null;
|
|
11310
|
+
}
|
|
11234
11311
|
function goSqlEndpointsFromFile(file, serviceDir) {
|
|
11235
11312
|
if (import_node_path47.default.extname(file.path) !== ".go") return [];
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
const
|
|
11239
|
-
|
|
11240
|
-
);
|
|
11313
|
+
if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
|
|
11314
|
+
const tree = parseSource10(makeGoParser3(), file.content);
|
|
11315
|
+
const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
|
|
11316
|
+
const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
|
|
11317
|
+
if (!importsDatabaseSql && !importsSqlx) return [];
|
|
11241
11318
|
const out = [];
|
|
11242
11319
|
walk7(tree.rootNode, (node) => {
|
|
11243
11320
|
if (node.type !== "call_expression") return;
|
|
11244
11321
|
const fn = node.childForFieldName("function");
|
|
11245
11322
|
if (fn?.type !== "selector_expression") return;
|
|
11246
11323
|
const method = fn.childForFieldName("field")?.text;
|
|
11247
|
-
if (!method
|
|
11248
|
-
const
|
|
11249
|
-
if (!
|
|
11250
|
-
const sql =
|
|
11324
|
+
if (!method) return;
|
|
11325
|
+
const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
|
|
11326
|
+
if (!recognized) return;
|
|
11327
|
+
const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
|
|
11328
|
+
if (sql === null) return;
|
|
11251
11329
|
const table = tableFromSqlStatement(sql);
|
|
11252
11330
|
if (!table) return;
|
|
11331
|
+
const columns = columnsFromSqlStatement(sql);
|
|
11253
11332
|
const line = node.startPosition.row + 1;
|
|
11254
11333
|
out.push({
|
|
11255
11334
|
infraId: (0, import_types35.infraId)("sql-table", table),
|
|
@@ -11257,7 +11336,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
|
|
|
11257
11336
|
kind: "sql-table",
|
|
11258
11337
|
edgeType: "CALLS",
|
|
11259
11338
|
confidenceKind: "verified-call-site",
|
|
11260
|
-
|
|
11339
|
+
...columns.length > 0 ? { columns } : {},
|
|
11340
|
+
evidence: {
|
|
11341
|
+
file: toPosix(import_node_path47.default.relative(serviceDir, file.path)),
|
|
11342
|
+
line,
|
|
11343
|
+
snippet: snippet(file.content, line)
|
|
11344
|
+
}
|
|
11261
11345
|
});
|
|
11262
11346
|
});
|
|
11263
11347
|
return out;
|
|
@@ -11271,12 +11355,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
|
|
|
11271
11355
|
var import_types36 = require("@neat.is/types");
|
|
11272
11356
|
var GORM_IMPORT_RE = /gorm\.io\/gorm/;
|
|
11273
11357
|
var PARSE_CHUNK11 = 16384;
|
|
11274
|
-
function
|
|
11358
|
+
function makeGoParser4() {
|
|
11275
11359
|
const p = new import_tree_sitter15.default();
|
|
11276
11360
|
p.setLanguage(import_tree_sitter_go4.default);
|
|
11277
11361
|
return p;
|
|
11278
11362
|
}
|
|
11279
|
-
function
|
|
11363
|
+
function parseSource11(parser, source) {
|
|
11280
11364
|
return parser.parse(
|
|
11281
11365
|
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
|
|
11282
11366
|
);
|
|
@@ -11700,7 +11784,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
|
|
|
11700
11784
|
function gormEndpointsFromFile(file, serviceDir) {
|
|
11701
11785
|
if (import_node_path48.default.extname(file.path) !== ".go") return [];
|
|
11702
11786
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
11703
|
-
const tree =
|
|
11787
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
11704
11788
|
const { structs, models, tableFor } = analyze(tree);
|
|
11705
11789
|
const out = [];
|
|
11706
11790
|
const seenTables = /* @__PURE__ */ new Set();
|
|
@@ -11731,7 +11815,7 @@ function gormEndpointsFromFile(file, serviceDir) {
|
|
|
11731
11815
|
function gormForeignKeys(file, serviceDir) {
|
|
11732
11816
|
if (import_node_path48.default.extname(file.path) !== ".go") return [];
|
|
11733
11817
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
11734
|
-
const tree =
|
|
11818
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
11735
11819
|
const { structs, models, tableFor } = analyze(tree);
|
|
11736
11820
|
const out = [];
|
|
11737
11821
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13474,7 +13558,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
|
|
|
13474
13558
|
init_cjs_shims();
|
|
13475
13559
|
var import_fastify2 = __toESM(require("fastify"), 1);
|
|
13476
13560
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
13477
|
-
var
|
|
13561
|
+
var import_types85 = require("@neat.is/types");
|
|
13478
13562
|
|
|
13479
13563
|
// src/extend/index.ts
|
|
13480
13564
|
init_cjs_shims();
|
|
@@ -14980,6 +15064,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
|
|
|
14980
15064
|
unresolved++;
|
|
14981
15065
|
continue;
|
|
14982
15066
|
}
|
|
15067
|
+
if (signal.incident) {
|
|
15068
|
+
ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
15069
|
+
if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
|
|
15070
|
+
unresolved++;
|
|
15071
|
+
continue;
|
|
15072
|
+
}
|
|
15073
|
+
await appendConnectorIncident(ctx.errorsPath, {
|
|
15074
|
+
id: signal.incident.id,
|
|
15075
|
+
timestamp: signal.incident.timestamp,
|
|
15076
|
+
service: signal.incident.service,
|
|
15077
|
+
errorType: signal.incident.errorType,
|
|
15078
|
+
errorMessage: signal.incident.errorMessage,
|
|
15079
|
+
...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
|
|
15080
|
+
affectedNode: resolved.targetNodeId
|
|
15081
|
+
});
|
|
15082
|
+
continue;
|
|
15083
|
+
}
|
|
14983
15084
|
if (resolved.ensureInfraNode) {
|
|
14984
15085
|
const { kind, name, provider } = resolved.ensureInfraNode;
|
|
14985
15086
|
ensureInfraNode(graph, kind, name, provider);
|
|
@@ -17214,6 +17315,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
|
|
|
17214
17315
|
};
|
|
17215
17316
|
}
|
|
17216
17317
|
|
|
17318
|
+
// src/connectors/eas/index.ts
|
|
17319
|
+
init_cjs_shims();
|
|
17320
|
+
|
|
17321
|
+
// src/connectors/eas/client.ts
|
|
17322
|
+
init_cjs_shims();
|
|
17323
|
+
|
|
17324
|
+
// src/connectors/eas/types.ts
|
|
17325
|
+
init_cjs_shims();
|
|
17326
|
+
function readEasCredentials(raw) {
|
|
17327
|
+
const token = raw["token"];
|
|
17328
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
17329
|
+
throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
|
|
17330
|
+
}
|
|
17331
|
+
return { token };
|
|
17332
|
+
}
|
|
17333
|
+
var EAS_STATUS_ERRORED = "ERRORED";
|
|
17334
|
+
var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
|
|
17335
|
+
"SPIN_UP_BUILDER",
|
|
17336
|
+
"PREPARE_CREDENTIALS",
|
|
17337
|
+
"RESTORE_CACHE",
|
|
17338
|
+
"UPLOAD_APPLICATION_ARCHIVE"
|
|
17339
|
+
]);
|
|
17340
|
+
var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
|
|
17341
|
+
function isTransientFailure(err) {
|
|
17342
|
+
if (!err) return false;
|
|
17343
|
+
const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
|
|
17344
|
+
if (phase) {
|
|
17345
|
+
if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
|
|
17346
|
+
if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
|
|
17347
|
+
}
|
|
17348
|
+
const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
|
|
17349
|
+
if (code && INTERNAL_ERROR_CODE.test(code)) return true;
|
|
17350
|
+
return false;
|
|
17351
|
+
}
|
|
17352
|
+
var FIELD_SEP3 = "\0";
|
|
17353
|
+
var EAS_TARGET_KIND = "eas-build";
|
|
17354
|
+
function packEasTargetName(identity) {
|
|
17355
|
+
return [identity.serviceName, identity.phase].join(FIELD_SEP3);
|
|
17356
|
+
}
|
|
17357
|
+
function parseEasTargetName(targetName) {
|
|
17358
|
+
const sep = targetName.indexOf(FIELD_SEP3);
|
|
17359
|
+
if (sep === -1) return null;
|
|
17360
|
+
const serviceName = targetName.slice(0, sep);
|
|
17361
|
+
const phase = targetName.slice(sep + 1);
|
|
17362
|
+
if (!serviceName) return null;
|
|
17363
|
+
return { serviceName, phase };
|
|
17364
|
+
}
|
|
17365
|
+
|
|
17366
|
+
// src/connectors/eas/client.ts
|
|
17367
|
+
var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
|
|
17368
|
+
var DEFAULT_PAGE_SIZE = 50;
|
|
17369
|
+
var DEFAULT_MAX_PAGES = 10;
|
|
17370
|
+
var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
|
|
17371
|
+
var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
|
|
17372
|
+
var BUILDS_QUERY = `
|
|
17373
|
+
query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
|
|
17374
|
+
app {
|
|
17375
|
+
byId(appId: $appId) {
|
|
17376
|
+
builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
|
|
17377
|
+
id
|
|
17378
|
+
status
|
|
17379
|
+
platform
|
|
17380
|
+
buildProfile
|
|
17381
|
+
gitCommitHash
|
|
17382
|
+
gitCommitMessage
|
|
17383
|
+
gitRef
|
|
17384
|
+
isGitWorkingTreeDirty
|
|
17385
|
+
createdAt
|
|
17386
|
+
completedAt
|
|
17387
|
+
error {
|
|
17388
|
+
buildPhase
|
|
17389
|
+
errorCode
|
|
17390
|
+
message
|
|
17391
|
+
docsUrl
|
|
17392
|
+
}
|
|
17393
|
+
logFileUrls
|
|
17394
|
+
}
|
|
17395
|
+
}
|
|
17396
|
+
}
|
|
17397
|
+
}
|
|
17398
|
+
`;
|
|
17399
|
+
async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
|
|
17400
|
+
const res = await junctionFetch(
|
|
17401
|
+
apiUrl,
|
|
17402
|
+
{
|
|
17403
|
+
method: "POST",
|
|
17404
|
+
headers: {
|
|
17405
|
+
"Content-Type": "application/json",
|
|
17406
|
+
...bearerAuthHeader(token)
|
|
17407
|
+
},
|
|
17408
|
+
body: JSON.stringify({ query, variables })
|
|
17409
|
+
},
|
|
17410
|
+
// accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
|
|
17411
|
+
// bucket (ADR-131), the closest thing this connector carries to "one account".
|
|
17412
|
+
{ provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
|
|
17413
|
+
);
|
|
17414
|
+
if (!res.ok) {
|
|
17415
|
+
throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
17416
|
+
}
|
|
17417
|
+
const body = await res.json();
|
|
17418
|
+
if (body.errors && body.errors.length > 0) {
|
|
17419
|
+
throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
17420
|
+
}
|
|
17421
|
+
if (!body.data) throw new Error("Expo GraphQL response carried no data");
|
|
17422
|
+
return body.data;
|
|
17423
|
+
}
|
|
17424
|
+
async function fetchErroredBuilds(token, config, fetchImpl) {
|
|
17425
|
+
const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
|
|
17426
|
+
const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
17427
|
+
const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
|
|
17428
|
+
const out = [];
|
|
17429
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17430
|
+
for (let page = 0; page < maxPages; page++) {
|
|
17431
|
+
const data = await easGraphQL(
|
|
17432
|
+
apiUrl,
|
|
17433
|
+
token,
|
|
17434
|
+
BUILDS_QUERY,
|
|
17435
|
+
{ appId: config.appId, offset: page * pageSize, limit: pageSize },
|
|
17436
|
+
config.appId,
|
|
17437
|
+
fetchImpl
|
|
17438
|
+
);
|
|
17439
|
+
const builds = data.app?.byId?.builds;
|
|
17440
|
+
if (!Array.isArray(builds)) break;
|
|
17441
|
+
let added = 0;
|
|
17442
|
+
for (const b of builds) {
|
|
17443
|
+
if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
|
|
17444
|
+
if (b.status !== EAS_STATUS_ERRORED) continue;
|
|
17445
|
+
if (seen.has(b.id)) continue;
|
|
17446
|
+
seen.add(b.id);
|
|
17447
|
+
out.push(b);
|
|
17448
|
+
added++;
|
|
17449
|
+
}
|
|
17450
|
+
if (builds.length < pageSize) break;
|
|
17451
|
+
if (added === 0) break;
|
|
17452
|
+
}
|
|
17453
|
+
return out;
|
|
17454
|
+
}
|
|
17455
|
+
async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
|
|
17456
|
+
if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
|
|
17457
|
+
const doFetch = fetchImpl ?? fetch;
|
|
17458
|
+
const chunks = [];
|
|
17459
|
+
for (const url of logFileUrls) {
|
|
17460
|
+
if (typeof url !== "string" || url.length === 0) continue;
|
|
17461
|
+
try {
|
|
17462
|
+
const res = await doFetch(url);
|
|
17463
|
+
if (!res.ok) continue;
|
|
17464
|
+
chunks.push(await res.text());
|
|
17465
|
+
} catch {
|
|
17466
|
+
}
|
|
17467
|
+
}
|
|
17468
|
+
const joined = chunks.join("\n");
|
|
17469
|
+
return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
|
|
17470
|
+
}
|
|
17471
|
+
|
|
17472
|
+
// src/connectors/eas/map.ts
|
|
17473
|
+
init_cjs_shims();
|
|
17474
|
+
function buildEventTime(build) {
|
|
17475
|
+
if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
|
|
17476
|
+
if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
|
|
17477
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
17478
|
+
}
|
|
17479
|
+
function incidentMessage2(build) {
|
|
17480
|
+
const err = build.error ?? {};
|
|
17481
|
+
const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
|
|
17482
|
+
const detail = typeof err.message === "string" && err.message.trim().length > 0 && err.message.trim() || typeof err.errorCode === "string" && err.errorCode.length > 0 && err.errorCode || "no error detail reported";
|
|
17483
|
+
let msg = `EAS build failed${phase}: ${detail}`;
|
|
17484
|
+
if (build.isGitWorkingTreeDirty === true) {
|
|
17485
|
+
msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
|
|
17486
|
+
}
|
|
17487
|
+
return msg;
|
|
17488
|
+
}
|
|
17489
|
+
function incidentAttributes(build) {
|
|
17490
|
+
const attrs = {};
|
|
17491
|
+
const err = build.error ?? {};
|
|
17492
|
+
const put = (k, v) => {
|
|
17493
|
+
if (typeof v === "string" && v.length === 0) return;
|
|
17494
|
+
if (v !== void 0 && v !== null) attrs[k] = v;
|
|
17495
|
+
};
|
|
17496
|
+
put("eas.buildId", build.id);
|
|
17497
|
+
put("eas.platform", build.platform ?? void 0);
|
|
17498
|
+
put("eas.buildProfile", build.buildProfile ?? void 0);
|
|
17499
|
+
put("eas.buildPhase", err.buildPhase ?? void 0);
|
|
17500
|
+
put("eas.errorCode", err.errorCode ?? void 0);
|
|
17501
|
+
put("eas.docsUrl", err.docsUrl ?? void 0);
|
|
17502
|
+
put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
|
|
17503
|
+
put("eas.gitRef", build.gitRef ?? void 0);
|
|
17504
|
+
put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
|
|
17505
|
+
if (typeof build.isGitWorkingTreeDirty === "boolean") {
|
|
17506
|
+
attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
|
|
17507
|
+
if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
|
|
17508
|
+
}
|
|
17509
|
+
put("eas.createdAt", build.createdAt ?? void 0);
|
|
17510
|
+
put("eas.completedAt", build.completedAt ?? void 0);
|
|
17511
|
+
if (typeof build.logsText === "string" && build.logsText.length > 0) {
|
|
17512
|
+
attrs["eas.logs"] = build.logsText;
|
|
17513
|
+
}
|
|
17514
|
+
return attrs;
|
|
17515
|
+
}
|
|
17516
|
+
function mapBuildToSignal(build, serviceName) {
|
|
17517
|
+
if (!build || typeof build !== "object") return null;
|
|
17518
|
+
if (build.status !== EAS_STATUS_ERRORED) return null;
|
|
17519
|
+
if (!build.error) return null;
|
|
17520
|
+
if (isTransientFailure(build.error)) return null;
|
|
17521
|
+
const timestamp = buildEventTime(build);
|
|
17522
|
+
const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
|
|
17523
|
+
return {
|
|
17524
|
+
targetKind: EAS_TARGET_KIND,
|
|
17525
|
+
targetName: packEasTargetName({ serviceName, phase }),
|
|
17526
|
+
// Incident-only — no edge, so no call/error count to replay.
|
|
17527
|
+
callCount: 0,
|
|
17528
|
+
errorCount: 0,
|
|
17529
|
+
lastObservedIso: timestamp,
|
|
17530
|
+
incident: {
|
|
17531
|
+
id: `eas:build:${build.id}`,
|
|
17532
|
+
timestamp,
|
|
17533
|
+
service: serviceName,
|
|
17534
|
+
errorType: "eas-build-failure",
|
|
17535
|
+
errorMessage: incidentMessage2(build),
|
|
17536
|
+
attributes: incidentAttributes(build)
|
|
17537
|
+
}
|
|
17538
|
+
};
|
|
17539
|
+
}
|
|
17540
|
+
function mapBuildsToSignals(builds, serviceName) {
|
|
17541
|
+
const out = [];
|
|
17542
|
+
for (const build of builds) {
|
|
17543
|
+
const signal = mapBuildToSignal(build, serviceName);
|
|
17544
|
+
if (signal) out.push(signal);
|
|
17545
|
+
}
|
|
17546
|
+
return out;
|
|
17547
|
+
}
|
|
17548
|
+
|
|
17549
|
+
// src/connectors/eas/resolve.ts
|
|
17550
|
+
init_cjs_shims();
|
|
17551
|
+
var import_types82 = require("@neat.is/types");
|
|
17552
|
+
var NO_ENV2 = "unknown";
|
|
17553
|
+
var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
|
|
17554
|
+
var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
|
|
17555
|
+
"READ_APP_CONFIG",
|
|
17556
|
+
"CONFIGURE_EXPO_UPDATES",
|
|
17557
|
+
"CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
|
|
17558
|
+
]);
|
|
17559
|
+
function configBasenamesForPhase(phase) {
|
|
17560
|
+
if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
|
|
17561
|
+
if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
|
|
17562
|
+
return [];
|
|
17563
|
+
}
|
|
17564
|
+
function configNodeService(graph, configNodeId) {
|
|
17565
|
+
for (const edgeId of graph.inboundEdges(configNodeId)) {
|
|
17566
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
17567
|
+
if (edge.type !== import_types82.EdgeType.CONFIGURED_BY) continue;
|
|
17568
|
+
const parsed = (0, import_types82.parseFileId)(edge.source);
|
|
17569
|
+
if (parsed) return parsed.service;
|
|
17570
|
+
}
|
|
17571
|
+
return null;
|
|
17572
|
+
}
|
|
17573
|
+
function findConfigNode(graph, basenames, serviceName) {
|
|
17574
|
+
let scoped = null;
|
|
17575
|
+
let anyMatch = null;
|
|
17576
|
+
graph.forEachNode((id, attrs) => {
|
|
17577
|
+
if (scoped) return;
|
|
17578
|
+
const node = attrs;
|
|
17579
|
+
if (node.type !== import_types82.NodeType.ConfigNode) return;
|
|
17580
|
+
if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
|
|
17581
|
+
if (anyMatch === null) anyMatch = id;
|
|
17582
|
+
if (configNodeService(graph, id) === serviceName) scoped = id;
|
|
17583
|
+
});
|
|
17584
|
+
return scoped ?? anyMatch;
|
|
17585
|
+
}
|
|
17586
|
+
function createEasResolveTarget(graph) {
|
|
17587
|
+
return (signal) => {
|
|
17588
|
+
if (signal.targetKind !== EAS_TARGET_KIND) return null;
|
|
17589
|
+
const identity = parseEasTargetName(signal.targetName);
|
|
17590
|
+
if (!identity) return null;
|
|
17591
|
+
const { serviceName, phase } = identity;
|
|
17592
|
+
const basenames = configBasenamesForPhase(phase);
|
|
17593
|
+
if (basenames.length > 0) {
|
|
17594
|
+
const configNodeId = findConfigNode(graph, basenames, serviceName);
|
|
17595
|
+
if (configNodeId) {
|
|
17596
|
+
return { targetNodeId: configNodeId, serviceName, edgeType: import_types82.EdgeType.CALLS };
|
|
17597
|
+
}
|
|
17598
|
+
}
|
|
17599
|
+
return {
|
|
17600
|
+
targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
|
|
17601
|
+
serviceName,
|
|
17602
|
+
edgeType: import_types82.EdgeType.CALLS
|
|
17603
|
+
};
|
|
17604
|
+
};
|
|
17605
|
+
}
|
|
17606
|
+
|
|
17607
|
+
// src/connectors/eas/index.ts
|
|
17608
|
+
function isBuildSince(build, sinceIso) {
|
|
17609
|
+
const t = Date.parse(buildEventTime(build));
|
|
17610
|
+
const s = Date.parse(sinceIso);
|
|
17611
|
+
if (Number.isNaN(t) || Number.isNaN(s)) return true;
|
|
17612
|
+
return t > s;
|
|
17613
|
+
}
|
|
17614
|
+
function boundedSinceIso2(since, now, maxLookbackMs) {
|
|
17615
|
+
const floor = new Date(now.getTime() - maxLookbackMs);
|
|
17616
|
+
if (!since) return floor.toISOString();
|
|
17617
|
+
const sinceMs = new Date(since).getTime();
|
|
17618
|
+
if (Number.isNaN(sinceMs)) return floor.toISOString();
|
|
17619
|
+
return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
|
|
17620
|
+
}
|
|
17621
|
+
var EasConnector = class {
|
|
17622
|
+
constructor(config, fetchImpl) {
|
|
17623
|
+
this.config = config;
|
|
17624
|
+
this.fetchImpl = fetchImpl;
|
|
17625
|
+
}
|
|
17626
|
+
config;
|
|
17627
|
+
fetchImpl;
|
|
17628
|
+
provider = "eas";
|
|
17629
|
+
async poll(ctx) {
|
|
17630
|
+
const creds = readEasCredentials(ctx.credentials);
|
|
17631
|
+
const serviceName = this.config.serviceName ?? this.config.appId;
|
|
17632
|
+
const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
|
|
17633
|
+
const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
|
|
17634
|
+
const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
|
|
17635
|
+
const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
|
|
17636
|
+
const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
|
|
17637
|
+
for (const build of fresh) {
|
|
17638
|
+
build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
|
|
17639
|
+
}
|
|
17640
|
+
return mapBuildsToSignals(fresh, serviceName);
|
|
17641
|
+
}
|
|
17642
|
+
};
|
|
17643
|
+
function createEasConnector(graph, config, fetchImpl) {
|
|
17644
|
+
return {
|
|
17645
|
+
connector: new EasConnector(config, fetchImpl),
|
|
17646
|
+
resolveTarget: createEasResolveTarget(graph)
|
|
17647
|
+
};
|
|
17648
|
+
}
|
|
17649
|
+
|
|
17217
17650
|
// src/connectors/registry.ts
|
|
17218
17651
|
var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
17219
17652
|
async function authProbe(input) {
|
|
@@ -17501,6 +17934,41 @@ var PROVIDER_DISPATCH = {
|
|
|
17501
17934
|
...fetchImpl ? { fetchImpl } : {}
|
|
17502
17935
|
});
|
|
17503
17936
|
}
|
|
17937
|
+
},
|
|
17938
|
+
eas: {
|
|
17939
|
+
provider: "eas",
|
|
17940
|
+
// The secret is a single robot-user EXPO_TOKEN; a single-string credential
|
|
17941
|
+
// maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
|
|
17942
|
+
primaryCredentialKey: "token",
|
|
17943
|
+
requiredCredentialFields: ["token"],
|
|
17944
|
+
requiredOptionFields: ["appId"],
|
|
17945
|
+
build(graph, options) {
|
|
17946
|
+
return createEasConnector(graph, options);
|
|
17947
|
+
},
|
|
17948
|
+
// Runs the connector's real `builds` query at limit 1 — the exact read poll()
|
|
17949
|
+
// performs, minus the pages — so the probe checks both that the EXPO_TOKEN
|
|
17950
|
+
// authenticates and that this app id is reachable, the same probe-the-real-
|
|
17951
|
+
// query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
|
|
17952
|
+
// A bad or wrong-scoped token comes back as an Expo GraphQL error, which
|
|
17953
|
+
// `fetchErroredBuilds` throws on, so it fails honestly here rather than
|
|
17954
|
+
// silently at the first poll.
|
|
17955
|
+
async validate({ credentials, options, fetchImpl }) {
|
|
17956
|
+
const cfg = options;
|
|
17957
|
+
const appId = String(cfg.appId ?? "");
|
|
17958
|
+
if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
|
|
17959
|
+
const probeConfig = {
|
|
17960
|
+
appId,
|
|
17961
|
+
pageSize: 1,
|
|
17962
|
+
maxPages: 1,
|
|
17963
|
+
...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
|
|
17964
|
+
};
|
|
17965
|
+
try {
|
|
17966
|
+
await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
|
|
17967
|
+
return { ok: true };
|
|
17968
|
+
} catch (err) {
|
|
17969
|
+
return { ok: false, reason: `eas auth check failed: ${err.message}` };
|
|
17970
|
+
}
|
|
17971
|
+
}
|
|
17504
17972
|
}
|
|
17505
17973
|
};
|
|
17506
17974
|
function vercelCredsFrom(credentials) {
|
|
@@ -17682,7 +18150,11 @@ async function startConnectorPolling(input) {
|
|
|
17682
18150
|
const stopFns = all.map(
|
|
17683
18151
|
(registration) => startConnectorPollLoop(
|
|
17684
18152
|
registration.connector,
|
|
17685
|
-
{
|
|
18153
|
+
{
|
|
18154
|
+
projectDir: input.projectDir,
|
|
18155
|
+
credentials: registration.credentials,
|
|
18156
|
+
...input.errorsPath ? { errorsPath: input.errorsPath } : {}
|
|
18157
|
+
},
|
|
17686
18158
|
input.graph,
|
|
17687
18159
|
registration.resolveTarget,
|
|
17688
18160
|
{ intervalMs: registration.intervalMs, connectorId: registration.id }
|
|
@@ -17852,11 +18324,11 @@ function registerRoutes(scope, ctx) {
|
|
|
17852
18324
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
17853
18325
|
const parsed = [];
|
|
17854
18326
|
for (const c of candidates) {
|
|
17855
|
-
const r =
|
|
18327
|
+
const r = import_types85.DivergenceTypeSchema.safeParse(c);
|
|
17856
18328
|
if (!r.success) {
|
|
17857
18329
|
return reply.code(400).send({
|
|
17858
18330
|
error: `unknown divergence type "${c}"`,
|
|
17859
|
-
allowed:
|
|
18331
|
+
allowed: import_types85.DivergenceTypeSchema.options
|
|
17860
18332
|
});
|
|
17861
18333
|
}
|
|
17862
18334
|
parsed.push(r.data);
|
|
@@ -17963,10 +18435,15 @@ function registerRoutes(scope, ctx) {
|
|
|
17963
18435
|
}
|
|
17964
18436
|
const reg = built.registration;
|
|
17965
18437
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
18438
|
+
const incidentsPath = errorsPathFor(proj);
|
|
17966
18439
|
try {
|
|
17967
18440
|
const result = await ctx.runPoll(
|
|
17968
18441
|
reg.connector,
|
|
17969
|
-
{
|
|
18442
|
+
{
|
|
18443
|
+
projectDir: proj.scanPath ?? "",
|
|
18444
|
+
credentials: reg.credentials,
|
|
18445
|
+
...incidentsPath ? { errorsPath: incidentsPath } : {}
|
|
18446
|
+
},
|
|
17970
18447
|
proj.graph,
|
|
17971
18448
|
reg.resolveTarget
|
|
17972
18449
|
);
|
|
@@ -18165,7 +18642,7 @@ function registerRoutes(scope, ctx) {
|
|
|
18165
18642
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
18166
18643
|
let violations = await log.readAll();
|
|
18167
18644
|
if (req.query.severity) {
|
|
18168
|
-
const sev =
|
|
18645
|
+
const sev = import_types85.PolicySeveritySchema.safeParse(req.query.severity);
|
|
18169
18646
|
if (!sev.success) {
|
|
18170
18647
|
return reply.code(400).send({
|
|
18171
18648
|
error: "invalid severity",
|
|
@@ -18204,7 +18681,7 @@ function registerRoutes(scope, ctx) {
|
|
|
18204
18681
|
scope.post("/policies/check", async (req, reply) => {
|
|
18205
18682
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18206
18683
|
if (!proj) return;
|
|
18207
|
-
const parsed =
|
|
18684
|
+
const parsed = import_types85.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
18208
18685
|
if (!parsed.success) {
|
|
18209
18686
|
return reply.code(400).send({
|
|
18210
18687
|
error: "invalid /policies/check body",
|
|
@@ -18553,7 +19030,7 @@ function unroutedErrorsPath(neatHome3) {
|
|
|
18553
19030
|
}
|
|
18554
19031
|
|
|
18555
19032
|
// src/daemon.ts
|
|
18556
|
-
var
|
|
19033
|
+
var import_types86 = require("@neat.is/types");
|
|
18557
19034
|
function daemonJsonPath(scanPath) {
|
|
18558
19035
|
return import_node_path67.default.join(scanPath, "neat-out", "daemon.json");
|
|
18559
19036
|
}
|
|
@@ -18678,7 +19155,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
|
18678
19155
|
if (!serviceName) return true;
|
|
18679
19156
|
if (serviceNameMatchesProject(serviceName, project)) return true;
|
|
18680
19157
|
return graph.someNode(
|
|
18681
|
-
(_id, attrs) => attrs.type ===
|
|
19158
|
+
(_id, attrs) => attrs.type === import_types86.NodeType.ServiceNode && attrs.name === serviceName
|
|
18682
19159
|
);
|
|
18683
19160
|
}
|
|
18684
19161
|
async function bootstrapProject(entry, connectors = [], neatHome3) {
|
|
@@ -18726,6 +19203,10 @@ async function bootstrapProject(entry, connectors = [], neatHome3) {
|
|
|
18726
19203
|
project: entry.name,
|
|
18727
19204
|
graph,
|
|
18728
19205
|
projectDir: entry.path,
|
|
19206
|
+
// The slot's incident ledger, so an incident-emitting connector (ADR-185)
|
|
19207
|
+
// writes a build-failure incident onto the same errors.ndjson OTLP-derived
|
|
19208
|
+
// incidents land in.
|
|
19209
|
+
errorsPath: paths.errorsPath,
|
|
18729
19210
|
...neatHome3 ? { home: neatHome3 } : {},
|
|
18730
19211
|
extra: connectors,
|
|
18731
19212
|
onSkip: (skipped, reason) => console.warn(
|