primitive-admin 1.1.0-alpha.82 → 1.1.0-alpha.83
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/src/commands/documents.js +333 -42
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/functions.js +302 -120
- package/dist/src/commands/functions.js.map +1 -1
- package/dist/src/commands/locks.js +15 -1
- package/dist/src/commands/locks.js.map +1 -1
- package/dist/src/commands/sync.d.ts +50 -0
- package/dist/src/commands/sync.js +560 -312
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +75 -2
- package/dist/src/lib/api-client.js +122 -7
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.js +5 -5
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
- package/dist/src/lib/document-ingest-artifact.js +505 -0
- package/dist/src/lib/document-ingest-artifact.js.map +1 -0
- package/dist/src/lib/document-ingest-input.d.ts +51 -0
- package/dist/src/lib/document-ingest-input.js +132 -0
- package/dist/src/lib/document-ingest-input.js.map +1 -0
- package/dist/src/lib/document-ingest.d.ts +78 -0
- package/dist/src/lib/document-ingest.js +231 -0
- package/dist/src/lib/document-ingest.js.map +1 -0
- package/dist/src/lib/function-bundle.d.ts +6 -0
- package/dist/src/lib/function-bundle.js +8 -0
- package/dist/src/lib/function-bundle.js.map +1 -1
- package/dist/src/lib/function-collect.d.ts +1 -1
- package/dist/src/lib/function-collect.js +16 -0
- package/dist/src/lib/function-collect.js.map +1 -1
- package/dist/src/lib/function-db-types.d.ts +4 -0
- package/dist/src/lib/function-db-types.js +31 -2
- package/dist/src/lib/function-db-types.js.map +1 -1
- package/dist/src/lib/function-document-types.d.ts +83 -0
- package/dist/src/lib/function-document-types.js +133 -0
- package/dist/src/lib/function-document-types.js.map +1 -1
- package/dist/src/lib/function-grants-preflight.js +5 -0
- package/dist/src/lib/function-grants-preflight.js.map +1 -1
- package/dist/src/lib/function-log-lines.d.ts +76 -0
- package/dist/src/lib/function-log-lines.js +160 -0
- package/dist/src/lib/function-log-lines.js.map +1 -0
- package/dist/src/lib/function-log-row.d.ts +29 -0
- package/dist/src/lib/function-log-row.js +73 -0
- package/dist/src/lib/function-log-row.js.map +1 -0
- package/dist/src/lib/function-run.d.ts +53 -35
- package/dist/src/lib/function-run.js +55 -44
- package/dist/src/lib/function-run.js.map +1 -1
- package/dist/src/lib/function-schema-codegen.d.ts +6 -9
- package/dist/src/lib/function-schema-codegen.js +22 -35
- package/dist/src/lib/function-schema-codegen.js.map +1 -1
- package/dist/src/lib/function-sync.d.ts +226 -35
- package/dist/src/lib/function-sync.js +731 -99
- package/dist/src/lib/function-sync.js.map +1 -1
- package/dist/src/lib/function-versions.d.ts +122 -0
- package/dist/src/lib/function-versions.js +182 -0
- package/dist/src/lib/function-versions.js.map +1 -0
- package/dist/src/lib/generated-config-surfaces.d.ts +390 -146
- package/dist/src/lib/generated-config-surfaces.js +1207 -401
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/generated-sdk-types.d.ts +1 -1
- package/dist/src/lib/generated-sdk-types.js +1 -1
- package/dist/src/lib/generated-sdk-types.js.map +1 -1
- package/dist/src/lib/log-inspection.d.ts +2 -0
- package/dist/src/lib/log-inspection.js +4 -0
- package/dist/src/lib/log-inspection.js.map +1 -1
- package/dist/src/lib/output.d.ts +15 -0
- package/dist/src/lib/output.js +28 -0
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
- package/dist/src/lib/snapshot-audit-source.js +58 -0
- package/dist/src/lib/snapshot-audit-source.js.map +1 -0
- package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
- package/dist/src/lib/snapshot-audit-store.js +196 -0
- package/dist/src/lib/snapshot-audit-store.js.map +1 -0
- package/dist/src/lib/snapshot-audit.d.ts +207 -0
- package/dist/src/lib/snapshot-audit.js +431 -0
- package/dist/src/lib/snapshot-audit.js.map +1 -0
- package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
- package/dist/src/lib/snapshot-build-rows.js +87 -0
- package/dist/src/lib/snapshot-build-rows.js.map +1 -0
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +23 -16
- package/dist/src/lib/swift-codegen/functionGenerator.js +34 -33
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
- package/dist/src/lib/sync-selectors.d.ts +45 -2
- package/dist/src/lib/sync-selectors.js +66 -5
- package/dist/src/lib/sync-selectors.js.map +1 -1
- package/package.json +2 -2
|
@@ -37,7 +37,8 @@ import { buildPayloadFromToml, projectEntityToPayload, projectRecordToToml, toml
|
|
|
37
37
|
import { CONFIG_SURFACES, isEmptyForEmit, } from "../lib/generated-config-surfaces.js";
|
|
38
38
|
// #3179 — the function's two halves: the header payloads and the envelope
|
|
39
39
|
// `config push` sends, and the bytes `config pull` writes back.
|
|
40
|
-
import { applyFunctionPull, buildFunctionConfigPush, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, functionTomlPreflightErrors, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
|
|
40
|
+
import { applyFunctionPull, buildFunctionConfigPush, createDocumentSchemaDropWarner, documentSchemaRetentionFailure, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, functionTomlIgnoredKeyWarnings, functionTomlPreflightErrors, planDocumentSchemaPull, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
|
|
41
|
+
import { newerVersionLine, pullNewerVersionLine, rollbackDiffHint, } from "../lib/function-versions.js";
|
|
41
42
|
import { FunctionBuildError } from "../lib/function-bundle.js";
|
|
42
43
|
// #3182 — the local half of capability validation. The grammar is the vendored
|
|
43
44
|
// module the server also imports; this is where an author's own file is read.
|
|
@@ -48,6 +49,9 @@ import { functionTypeWiringProblems, staleFunctionTypeArtifacts, writeFunctionTy
|
|
|
48
49
|
// #3281 — the functions declaration and the per-key invokers ride the same
|
|
49
50
|
// plan; a malformed schema is their one refusal, named by file.
|
|
50
51
|
import { FunctionSchemaCodegenError } from "../lib/function-schema-codegen.js";
|
|
52
|
+
// #3455 — the one resolution rule push, pull and diff share for the project's
|
|
53
|
+
// document schema.
|
|
54
|
+
import { isFunctionDeclarationFile, resolveDocumentSchema, } from "../lib/function-document-types.js";
|
|
51
55
|
// #3331 — and the compiler is actually asked: a push holds the code it ships
|
|
52
56
|
// to the declarations it just generated.
|
|
53
57
|
import { createFunctionTypechecker, } from "../lib/function-typecheck.js";
|
|
@@ -60,7 +64,7 @@ import { slugifyTestCaseName, testCaseFileBasenames, } from "../lib/test-case-fi
|
|
|
60
64
|
// diff and their tests have always imported them from this module.
|
|
61
65
|
export { resolveSlugCollisions, slugifyTestCaseName, testCaseFileBasenames, } from "../lib/test-case-file-names.js";
|
|
62
66
|
import { success, error, printApiError, info, warn, keyValue, json, divider, } from "../lib/output.js";
|
|
63
|
-
import { filterFilesBySelection, parseSyncSelectors, SelectorError, selectionIncludesKey, selectionIncludesType, selectionIncludesApp, selectionIncludesAnyVar, unmatchedSelectors, } from "../lib/sync-selectors.js";
|
|
67
|
+
import { filterFilesBySelection, parseSyncSelectors, SelectorError, selectionIncludesKey, selectionIncludesType, selectionIncludesApp, selectionIncludesAnyVar, trackedSelectorKeys, unmatchedSelectors, } from "../lib/sync-selectors.js";
|
|
64
68
|
import { confirmPrompt, ConfirmPromptError } from "../lib/confirm-prompt.js";
|
|
65
69
|
import { signingSecretStatusWarning, signingSecretHealthWarning, previousSigningSecretWarning, } from "../lib/signing-secret-status.js";
|
|
66
70
|
import chalk from "chalk";
|
|
@@ -2185,6 +2189,348 @@ function localConfigRowKey(label, tomlData, file) {
|
|
|
2185
2189
|
return fallback;
|
|
2186
2190
|
}
|
|
2187
2191
|
}
|
|
2192
|
+
/**
|
|
2193
|
+
* `config push`'s preflight — every check the authored files decide on their
|
|
2194
|
+
* own, run before the FIRST mutating call (#976 fix A, #3320, #3375).
|
|
2195
|
+
*
|
|
2196
|
+
* The block below was inline inside the push command's `.action()` callback
|
|
2197
|
+
* until #3375 lifted it here unchanged. It had to become an exported symbol
|
|
2198
|
+
* for the registry's promise to be checkable at all: a rule declared
|
|
2199
|
+
* `preflight` says it runs before `config push` mutates anything, and
|
|
2200
|
+
* `src/config-surface/preflight-reachability.ts` resolves that claim
|
|
2201
|
+
* statically from the declared `<module>#<export>` against exactly this entry
|
|
2202
|
+
* point. With the preflight anonymous, "reached from the preflight" had
|
|
2203
|
+
* nothing to resolve against, and the stage string was satisfied by typing it.
|
|
2204
|
+
*
|
|
2205
|
+
* Its boundary is therefore precise and load-bearing: this function is what
|
|
2206
|
+
* contributes to `preflightValidationErrors` and aborts. The advisory reads
|
|
2207
|
+
* that follow the abort in the action — the remote vars snapshot and both caps
|
|
2208
|
+
* — stay there and are declared `apply`, because the server is what enforces
|
|
2209
|
+
* them (`intent` §Non-goals, "Emptying the apply stage").
|
|
2210
|
+
*
|
|
2211
|
+
* A network call inside a preflight is not disqualifying: `listIntegrations`
|
|
2212
|
+
* was already here, answering "will this `integration:` grant have a target?"
|
|
2213
|
+
* against the live listing merged with the tree, and falling back to the tree
|
|
2214
|
+
* alone when the listing cannot be fetched.
|
|
2215
|
+
*
|
|
2216
|
+
* `input` names what the block used to close over. `client` is narrowed to the
|
|
2217
|
+
* one method it calls, so an apply call cannot drift in here later without
|
|
2218
|
+
* that being a visible change to this signature.
|
|
2219
|
+
*/
|
|
2220
|
+
export async function runConfigPushPreflight(input) {
|
|
2221
|
+
const { configDir, resolvedAppId, pushSelection, syncState, force, isCrossAppPush, client, } = input;
|
|
2222
|
+
/**
|
|
2223
|
+
* Narrow a type's file list to the `--only` selection — the same one-line
|
|
2224
|
+
* closure the action holds, rebuilt from the selection it passed in rather
|
|
2225
|
+
* than handed across the boundary as a function.
|
|
2226
|
+
*/
|
|
2227
|
+
const selectFiles = (label, dir, files) => filterFilesBySelection(pushSelection, label, dir, files);
|
|
2228
|
+
// ── Issue #976 (fix A): front-load ALL client-side TOML validation
|
|
2229
|
+
// before the first mutating call. Push is apply-as-you-go and processes
|
|
2230
|
+
// entities in a fixed sequence (cron triggers are created *before*
|
|
2231
|
+
// workflows are validated). Previously each entity's validation lived
|
|
2232
|
+
// inside its own apply loop and aborted via `process.exit(1)` — so a
|
|
2233
|
+
// known-bad sibling file (e.g. an invalid workflow TOML) could let an
|
|
2234
|
+
// earlier create (a cron trigger) be applied, then abort. The
|
|
2235
|
+
// `process.exit` also bypassed the save-on-failure catch below, so the
|
|
2236
|
+
// orphaned create never reached the sync-state baseline and the retry
|
|
2237
|
+
// re-issued a CREATE that the server rejected with 409 forever.
|
|
2238
|
+
//
|
|
2239
|
+
// Running every validation up-front means a known-bad file aborts
|
|
2240
|
+
// BEFORE anything is created. We collect all errors (not just the first)
|
|
2241
|
+
// and THROW — so the single save-on-failure catch is the only exit path
|
|
2242
|
+
// (no more `process.exit` bypass class). The per-loop validators below
|
|
2243
|
+
// remain as defense-in-depth but are converted to throws too.
|
|
2244
|
+
const preflightValidationErrors = [];
|
|
2245
|
+
// Validate all workflow TOMLs (#685 misnested-header check and the
|
|
2246
|
+
// #1447 system + accessRule lint).
|
|
2247
|
+
const preflightWorkflowsDir = join(configDir, "workflows");
|
|
2248
|
+
if (existsSync(preflightWorkflowsDir)) {
|
|
2249
|
+
// Under `--only` the preflight validates just the selected files:
|
|
2250
|
+
// an unrelated broken workflow elsewhere in the slot must not block
|
|
2251
|
+
// applying the one object the operator asked for.
|
|
2252
|
+
for (const file of selectFiles("workflow", preflightWorkflowsDir, readdirSync(preflightWorkflowsDir).filter((f) => f.endsWith(".toml")))) {
|
|
2253
|
+
const filePath = join(preflightWorkflowsDir, file);
|
|
2254
|
+
try {
|
|
2255
|
+
const tomlData = parseTomlFile(filePath);
|
|
2256
|
+
// #1660 — skip platform-owned workflows (the reserved
|
|
2257
|
+
// `__internal.*` namespace) BEFORE validating, mirroring the
|
|
2258
|
+
// apply loop below. `config push` validates every workflow TOML
|
|
2259
|
+
// here, up-front, before reaching that skip — so without this
|
|
2260
|
+
// second skip a stale/hollow `__internal.*.toml` with an invalid
|
|
2261
|
+
// `steps` shape would abort the whole push in preflight. Resolve
|
|
2262
|
+
// the key the same way the apply loop does (`[workflow].key`,
|
|
2263
|
+
// falling back to the filename) and match on the resolved key,
|
|
2264
|
+
// never the filename alone, so a renamed file is still caught.
|
|
2265
|
+
const platformOwnedKey = tomlData.workflow?.key || basename(file, ".toml");
|
|
2266
|
+
if (isPlatformOwnedWorkflowKey(platformOwnedKey)) {
|
|
2267
|
+
continue;
|
|
2268
|
+
}
|
|
2269
|
+
const tomlErrors = validateWorkflowToml(tomlData);
|
|
2270
|
+
if (tomlErrors.length > 0) {
|
|
2271
|
+
preflightValidationErrors.push(formatWorkflowTomlErrors(filePath, tomlErrors));
|
|
2272
|
+
}
|
|
2273
|
+
// #2643 — non-blocking notes (e.g. `strict` / `strictParams` are
|
|
2274
|
+
// now no-ops). Printed once per file at pre-flight so the author
|
|
2275
|
+
// sees them whether or not the push goes on to apply.
|
|
2276
|
+
for (const note of collectWorkflowTomlNotes(tomlData)) {
|
|
2277
|
+
console.log(` ${filePath}: ${note}`);
|
|
2278
|
+
}
|
|
2279
|
+
const unknownKeyError = workflowUnknownKeyError(filePath, tomlData);
|
|
2280
|
+
if (unknownKeyError) {
|
|
2281
|
+
preflightValidationErrors.push(` ${unknownKeyError}`);
|
|
2282
|
+
}
|
|
2283
|
+
// #2743 — the always-send update payload cannot carry a missing
|
|
2284
|
+
// `[workflow].name`. That is a local TOML error like every other
|
|
2285
|
+
// one here, so it belongs in the front-loaded pass (#976): caught
|
|
2286
|
+
// in the apply loop it would abort AFTER earlier resources (app
|
|
2287
|
+
// settings, cron triggers, …) had already been written, and
|
|
2288
|
+
// `--dry-run` — which never reaches the apply — would plan an
|
|
2289
|
+
// update the real push then refuses.
|
|
2290
|
+
//
|
|
2291
|
+
// Scoped to the workflows this push will actually UPDATE. A
|
|
2292
|
+
// create falls back to the manifest key, so an absent name is
|
|
2293
|
+
// legitimate authoring there — and once that create lands, the
|
|
2294
|
+
// same unchanged file must keep pushing clean (the apply loop
|
|
2295
|
+
// skips it on its content hash) instead of failing a push that
|
|
2296
|
+
// was never going to send it.
|
|
2297
|
+
if (workflowPushSendsUpdate(configDir, tomlData.workflow?.key || basename(file, ".toml"), tomlData, syncState, force)) {
|
|
2298
|
+
const nameError = workflowUpdateNameError(filePath, tomlData.workflow || {});
|
|
2299
|
+
if (nameError) {
|
|
2300
|
+
preflightValidationErrors.push(` ${nameError}`);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
catch (err) {
|
|
2305
|
+
preflightValidationErrors.push(` ${file}: ${err?.message || String(err)}`);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
// Validate all database-type TOMLs (issue #803 subscription
|
|
2310
|
+
// access/accessRule conflict + issue #752 $params references).
|
|
2311
|
+
const preflightDbTypesDir = join(configDir, "database-type-configs");
|
|
2312
|
+
if (existsSync(preflightDbTypesDir)) {
|
|
2313
|
+
for (const file of selectFiles("database-type-config", preflightDbTypesDir, readdirSync(preflightDbTypesDir).filter((f) => f.endsWith(".toml")))) {
|
|
2314
|
+
const filePath = join(preflightDbTypesDir, file);
|
|
2315
|
+
let rawToml;
|
|
2316
|
+
let tomlData;
|
|
2317
|
+
try {
|
|
2318
|
+
rawToml = readFileSync(filePath, "utf-8");
|
|
2319
|
+
tomlData = parseTomlFile(filePath);
|
|
2320
|
+
}
|
|
2321
|
+
catch (err) {
|
|
2322
|
+
preflightValidationErrors.push(` ${file}: ${err?.message || String(err)}`);
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
let operations = [];
|
|
2326
|
+
try {
|
|
2327
|
+
({ operations } = parseDatabaseTypeToml(tomlData));
|
|
2328
|
+
}
|
|
2329
|
+
catch (err) {
|
|
2330
|
+
if (err instanceof SubscriptionAccessKeyConflictError) {
|
|
2331
|
+
preflightValidationErrors.push(` ${file}: ${err.message}`);
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
preflightValidationErrors.push(` ${file}: ${err?.message || String(err)}`);
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
const validation = validateOperations({
|
|
2338
|
+
filePath,
|
|
2339
|
+
rawToml,
|
|
2340
|
+
operations,
|
|
2341
|
+
});
|
|
2342
|
+
for (const e of validation.errors) {
|
|
2343
|
+
preflightValidationErrors.push(` ${formatIssue(e)}`);
|
|
2344
|
+
}
|
|
2345
|
+
// Issue #1422: non-fatal deprecation notice. A `celContextAccess`
|
|
2346
|
+
// / `metadataAccess` gate on a database type config is a single CEL
|
|
2347
|
+
// expression that gates BOTH read and update of the database CEL
|
|
2348
|
+
// context — granting read implicitly grants update. Steer operators
|
|
2349
|
+
// to resource metadata categories (separate readRule/writeRule).
|
|
2350
|
+
// This is advisory only: the push continues and the runtime gate is
|
|
2351
|
+
// unchanged. It fires only when the deprecated key is actually
|
|
2352
|
+
// present, so metadata-category-only configs never trip it.
|
|
2353
|
+
const typeSection = tomlData.type || {};
|
|
2354
|
+
if (typeSection.celContextAccess || typeSection.metadataAccess) {
|
|
2355
|
+
const usedKey = typeSection.celContextAccess
|
|
2356
|
+
? "celContextAccess"
|
|
2357
|
+
: "metadataAccess";
|
|
2358
|
+
warn(` ${file}: \`${usedKey}\` is deprecated — it gates database CEL context READ and WRITE with one expression (read implies update). Prefer a resource metadata category with separate readRule/writeRule (issue #1420). Sync will continue; the runtime gate is unchanged.`);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
// Validate app.toml settings up-front (issue #1033). A mistyped value
|
|
2363
|
+
// is a hard error that must abort BEFORE any mutation — same
|
|
2364
|
+
// fail-before-mutate contract as the vars preflight below. The apply
|
|
2365
|
+
// block re-parses for the payload.
|
|
2366
|
+
// #2645 — bounded by `--only`, like the workflow and database-type
|
|
2367
|
+
// preflights above. `config push --only workflow/x` is the replacement for
|
|
2368
|
+
// `workflows update --from-file`, so it must work on a tree where
|
|
2369
|
+
// app.toml happens to be mid-edit.
|
|
2370
|
+
const preflightAppTomlPath = join(configDir, "app.toml");
|
|
2371
|
+
if (selectionIncludesApp(pushSelection) && existsSync(preflightAppTomlPath)) {
|
|
2372
|
+
try {
|
|
2373
|
+
const parsedApp = parseTomlFile(preflightAppTomlPath);
|
|
2374
|
+
const appErrors = collectAppSettingsPushErrors(parsedApp, {
|
|
2375
|
+
// #2880 DSO-001 — the same exclusion the apply block computes, so
|
|
2376
|
+
// the preflight validates the body that will actually be sent.
|
|
2377
|
+
excludeName: appSettingsNameExcluded({
|
|
2378
|
+
isCrossAppPush: !!isCrossAppPush,
|
|
2379
|
+
recordedExcludedName: syncState?.entities?.app?.excludedName,
|
|
2380
|
+
fileName: parsedApp?.app?.name,
|
|
2381
|
+
}),
|
|
2382
|
+
});
|
|
2383
|
+
for (const e of appErrors) {
|
|
2384
|
+
preflightValidationErrors.push(` app.toml: ${e}`);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
catch (err) {
|
|
2388
|
+
preflightValidationErrors.push(` app.toml: ${err?.message || String(err)}`);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
// Validate config vars up-front (issue #1423 review). Parse vars.toml
|
|
2392
|
+
// and check the same key/value constraints the server enforces here, in
|
|
2393
|
+
// the preflight pass, so an invalid entry aborts BEFORE any mutation
|
|
2394
|
+
// (app settings or a var upsert) is applied. Previously validation ran
|
|
2395
|
+
// mid-push, after app settings could already have been updated.
|
|
2396
|
+
// #2645 — bounded by `--only`, for the same reason as app.toml above.
|
|
2397
|
+
const preflightVarsTomlPath = join(configDir, "vars.toml");
|
|
2398
|
+
const varsSelected = selectionIncludesAnyVar(pushSelection);
|
|
2399
|
+
let preflightParsedVars = null;
|
|
2400
|
+
if (varsSelected && existsSync(preflightVarsTomlPath)) {
|
|
2401
|
+
try {
|
|
2402
|
+
const parsed = parseTomlFile(preflightVarsTomlPath);
|
|
2403
|
+
// #2645 — under `--only var/<key>` only the selected entries are
|
|
2404
|
+
// checked. The rest of the file is not being written, and aborting
|
|
2405
|
+
// over an entry this push will not touch is the same over-reach as
|
|
2406
|
+
// running the leg at all for an unselected singleton.
|
|
2407
|
+
for (const e of validateVarsFile(parsed, {
|
|
2408
|
+
onlyKeys: pushSelection ? pushSelection.varKeys : undefined,
|
|
2409
|
+
})) {
|
|
2410
|
+
preflightValidationErrors.push(` ${e}`);
|
|
2411
|
+
}
|
|
2412
|
+
preflightParsedVars = parsed;
|
|
2413
|
+
}
|
|
2414
|
+
catch (err) {
|
|
2415
|
+
preflightValidationErrors.push(` vars.toml: ${err?.message || String(err)}`);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
// #3182 — a function's capabilities, checked against the author's own
|
|
2419
|
+
// file before anything is applied. The grammar is purely local (and
|
|
2420
|
+
// since #3279 it keeps three kinds: `integration:<key>`,
|
|
2421
|
+
// `secret:<NAME>` and the high-blast opt-ins — a retired grant string
|
|
2422
|
+
// is refused here with the retirement); whether an `integration:`
|
|
2423
|
+
// names an integration that will exist is answered against the
|
|
2424
|
+
// tree's own `integrations/` (applied BEFORE functions) merged with
|
|
2425
|
+
// the integrations the server already has. If the live listing cannot
|
|
2426
|
+
// be fetched and the tree declares none, the target check is skipped
|
|
2427
|
+
// and the server — which is authoritative — has the last word.
|
|
2428
|
+
const preflightFunctionsDir = join(configDir, "functions");
|
|
2429
|
+
if (existsSync(preflightFunctionsDir)) {
|
|
2430
|
+
const functionFiles = selectFiles("function", preflightFunctionsDir, readdirSync(preflightFunctionsDir).filter(isFunctionDeclarationFile));
|
|
2431
|
+
if (functionFiles.length > 0) {
|
|
2432
|
+
const treeTargets = configObjectKeysFromTree(configDir);
|
|
2433
|
+
let knownTargets = null;
|
|
2434
|
+
try {
|
|
2435
|
+
const integrations = await client.listIntegrations(resolvedAppId, {
|
|
2436
|
+
status: "active",
|
|
2437
|
+
});
|
|
2438
|
+
knownTargets = mergeKnownGrantTargets({
|
|
2439
|
+
integrations: new Set((integrations?.items ?? [])
|
|
2440
|
+
.map((row) => String(row?.integrationKey ?? ""))
|
|
2441
|
+
.filter(Boolean)),
|
|
2442
|
+
}, treeTargets);
|
|
2443
|
+
}
|
|
2444
|
+
catch {
|
|
2445
|
+
knownTargets =
|
|
2446
|
+
treeTargets.integrations.size > 0 ? treeTargets : null;
|
|
2447
|
+
}
|
|
2448
|
+
for (const file of functionFiles) {
|
|
2449
|
+
const filePath = join(preflightFunctionsDir, file);
|
|
2450
|
+
let tomlData;
|
|
2451
|
+
try {
|
|
2452
|
+
tomlData = parseTomlFile(filePath);
|
|
2453
|
+
}
|
|
2454
|
+
catch (err) {
|
|
2455
|
+
preflightValidationErrors.push(` ${filePath}: ${err?.message || String(err)}`);
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
for (const message of functionGrantErrors({
|
|
2459
|
+
filePath,
|
|
2460
|
+
tomlData,
|
|
2461
|
+
knownTargets,
|
|
2462
|
+
})) {
|
|
2463
|
+
preflightValidationErrors.push(` ${message}`);
|
|
2464
|
+
}
|
|
2465
|
+
// #3320 — and everything else the file decides on its own: the
|
|
2466
|
+
// `entry`, the mode the two spellings resolve to, the trigger
|
|
2467
|
+
// rules, and a webhook trigger's signing-secret requirement and
|
|
2468
|
+
// reference shape. These used to be raised inside the apply loop,
|
|
2469
|
+
// after webhooks, integrations, vars and database types had
|
|
2470
|
+
// already been written — the partial tree this issue is about.
|
|
2471
|
+
// Bounded by the same `selectFiles` above, so `--only` keeps its
|
|
2472
|
+
// posture (#2645).
|
|
2473
|
+
const functionKey = tomlData?.function?.key || basename(file, ".toml");
|
|
2474
|
+
for (const message of functionTomlPreflightErrors({
|
|
2475
|
+
filePath,
|
|
2476
|
+
functionKey,
|
|
2477
|
+
tomlData,
|
|
2478
|
+
})) {
|
|
2479
|
+
preflightValidationErrors.push(` ${filePath}: ${message}`);
|
|
2480
|
+
}
|
|
2481
|
+
// #3482 — the retired runtime keys. NOT refusals through phase 4:
|
|
2482
|
+
// the file pushes with every other key applied, and the author
|
|
2483
|
+
// hears which line to delete before phase 5 refuses it. On
|
|
2484
|
+
// stderr, like every other advisory, so a scripted push reading
|
|
2485
|
+
// stdout is unaffected. Inside this loop rather than over the
|
|
2486
|
+
// whole tree, so `--only` bounds the advice as it bounds the
|
|
2487
|
+
// refusals (#2645).
|
|
2488
|
+
for (const message of functionTomlIgnoredKeyWarnings({
|
|
2489
|
+
functionKey,
|
|
2490
|
+
tomlData,
|
|
2491
|
+
})) {
|
|
2492
|
+
warn(` ${message}`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
// #2644 — reject any key the vendored definition does not recognize,
|
|
2498
|
+
// for every migrated configuration type. Same posture the `[workflow]`
|
|
2499
|
+
// check above has, generalized: a mistyped key fails here, in the
|
|
2500
|
+
// preflight pass, so nothing is applied and no request is issued —
|
|
2501
|
+
// rather than being dropped on the way to the server and reading as a
|
|
2502
|
+
// successful push that changed nothing.
|
|
2503
|
+
// #2880 — the same collector `config diff` runs, so the two commands
|
|
2504
|
+
// cannot disagree about which files are pushable.
|
|
2505
|
+
for (const rejected of collectConfigFileValidationErrors(configDir)) {
|
|
2506
|
+
for (const message of rejected.messages) {
|
|
2507
|
+
preflightValidationErrors.push(` ${message}`);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
// The `<key>.tests/*.toml` sidecars, which sit beside the block they
|
|
2511
|
+
// test rather than in a type directory of their own. Same posture,
|
|
2512
|
+
// reached through the registry's `test-case` surface — and through the
|
|
2513
|
+
// same collector `config diff` runs per sidecar (#2880), so neither
|
|
2514
|
+
// command can refuse a file the other calls Synced.
|
|
2515
|
+
for (const filePath of testCaseTomlFiles(configDir)) {
|
|
2516
|
+
let tomlData;
|
|
2517
|
+
try {
|
|
2518
|
+
tomlData = parseTomlFile(filePath);
|
|
2519
|
+
}
|
|
2520
|
+
catch {
|
|
2521
|
+
// Reported by the per-block test-case loop with its own message.
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
for (const message of testCaseSidecarPreflightErrors(filePath, tomlData)) {
|
|
2525
|
+
preflightValidationErrors.push(` ${message}`);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
if (preflightValidationErrors.length > 0) {
|
|
2529
|
+
throw new Error(`Aborting push: ${preflightValidationErrors.length} TOML validation error(s) — no changes were applied.\n` +
|
|
2530
|
+
preflightValidationErrors.join("\n"));
|
|
2531
|
+
}
|
|
2532
|
+
return { parsedVars: preflightParsedVars };
|
|
2533
|
+
}
|
|
2188
2534
|
/**
|
|
2189
2535
|
* Every per-entity config file whose TOML `config push` would refuse (#2880
|
|
2190
2536
|
* criterion 1, symmetry).
|
|
@@ -2206,7 +2552,11 @@ export function collectConfigFileValidationErrors(configDir) {
|
|
|
2206
2552
|
const typeDir = join(configDir, dir);
|
|
2207
2553
|
if (!existsSync(typeDir))
|
|
2208
2554
|
continue;
|
|
2209
|
-
for (const file of readdirSync(typeDir).filter((f) =>
|
|
2555
|
+
for (const file of readdirSync(typeDir).filter((f) =>
|
|
2556
|
+
// #3455 — `functions/` holds one GENERATED `.toml` beside the
|
|
2557
|
+
// declarations: the project's document schema, which is not a config
|
|
2558
|
+
// object and would be read as a function named after the file.
|
|
2559
|
+
dir === "functions" ? isFunctionDeclarationFile(f) : f.endsWith(".toml"))) {
|
|
2210
2560
|
const filePath = join(typeDir, file);
|
|
2211
2561
|
let tomlData;
|
|
2212
2562
|
try {
|
|
@@ -2264,7 +2614,7 @@ export function collectFunctionTomlValidationErrors(configDir) {
|
|
|
2264
2614
|
const functionsDir = join(configDir, "functions");
|
|
2265
2615
|
if (!existsSync(functionsDir))
|
|
2266
2616
|
return found;
|
|
2267
|
-
for (const file of readdirSync(functionsDir).filter(
|
|
2617
|
+
for (const file of readdirSync(functionsDir).filter(isFunctionDeclarationFile)) {
|
|
2268
2618
|
const filePath = join(functionsDir, file);
|
|
2269
2619
|
let tomlData;
|
|
2270
2620
|
try {
|
|
@@ -2287,6 +2637,35 @@ export function collectFunctionTomlValidationErrors(configDir) {
|
|
|
2287
2637
|
}
|
|
2288
2638
|
return found;
|
|
2289
2639
|
}
|
|
2640
|
+
/**
|
|
2641
|
+
* The retired runtime keys every `functions/*.toml` in the tree still carries
|
|
2642
|
+
* — #3482, the warning half of the collector above.
|
|
2643
|
+
*
|
|
2644
|
+
* Separate from the errors because it IS separate: a file carrying one of
|
|
2645
|
+
* these pushes, with every other key applied, so it is not a row `config diff`
|
|
2646
|
+
* calls invalid. It is a line an author reads on stderr from both commands, so
|
|
2647
|
+
* the two keep reading identically (the #2880 symmetry the errors collector
|
|
2648
|
+
* exists for, applied to advice rather than refusals).
|
|
2649
|
+
*/
|
|
2650
|
+
export function collectFunctionTomlIgnoredKeyWarnings(configDir) {
|
|
2651
|
+
const found = [];
|
|
2652
|
+
const functionsDir = join(configDir, "functions");
|
|
2653
|
+
if (!existsSync(functionsDir))
|
|
2654
|
+
return found;
|
|
2655
|
+
for (const file of readdirSync(functionsDir).filter(isFunctionDeclarationFile)) {
|
|
2656
|
+
const filePath = join(functionsDir, file);
|
|
2657
|
+
let tomlData;
|
|
2658
|
+
try {
|
|
2659
|
+
tomlData = parseTomlFile(filePath);
|
|
2660
|
+
}
|
|
2661
|
+
catch {
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2664
|
+
const key = tomlData?.function?.key || basename(file, ".toml");
|
|
2665
|
+
found.push(...functionTomlIgnoredKeyWarnings({ functionKey: key, tomlData }));
|
|
2666
|
+
}
|
|
2667
|
+
return found;
|
|
2668
|
+
}
|
|
2290
2669
|
/**
|
|
2291
2670
|
* The collectors' rows, one per diff row (#2880 symmetry, #3320).
|
|
2292
2671
|
*
|
|
@@ -4968,6 +5347,8 @@ export async function pullFunctions(client, appId, configDir, logger = () => { }
|
|
|
4968
5347
|
const functionsDir = join(configDir, "functions");
|
|
4969
5348
|
ensureDir(functionsDir);
|
|
4970
5349
|
const functionEntities = {};
|
|
5350
|
+
/** What each pulled version says about the document schema — CR3455-001. */
|
|
5351
|
+
const pulledSchemas = [];
|
|
4971
5352
|
let items = [];
|
|
4972
5353
|
try {
|
|
4973
5354
|
const result = await client.listFunctions(appId);
|
|
@@ -5015,11 +5396,21 @@ export async function pullFunctions(client, appId, configDir, logger = () => { }
|
|
|
5015
5396
|
}
|
|
5016
5397
|
let active;
|
|
5017
5398
|
let envelope;
|
|
5399
|
+
let newest = null;
|
|
5018
5400
|
try {
|
|
5019
|
-
|
|
5020
|
-
|
|
5401
|
+
// #3462 — the point read. The listing is a page now, so scanning it for
|
|
5402
|
+
// the id would miss a rolled-back function's active version past page
|
|
5403
|
+
// one; the separate one-row read below is only asking whether a NEWER
|
|
5404
|
+
// version exists, which is what the message after the write says.
|
|
5405
|
+
active = record.activeConfigId
|
|
5406
|
+
? await client.getFunctionConfig(appId, record.functionId, String(record.activeConfigId))
|
|
5407
|
+
: null;
|
|
5021
5408
|
if (active) {
|
|
5022
5409
|
envelope = await client.getFunctionConfigEnvelope(appId, record.functionId, active.configId);
|
|
5410
|
+
const page = await client.listFunctionConfigs(appId, record.functionId, {
|
|
5411
|
+
limit: 1,
|
|
5412
|
+
});
|
|
5413
|
+
newest = (page.items || [])[0] ?? null;
|
|
5023
5414
|
}
|
|
5024
5415
|
}
|
|
5025
5416
|
catch {
|
|
@@ -5032,9 +5423,30 @@ export async function pullFunctions(client, appId, configDir, logger = () => { }
|
|
|
5032
5423
|
logger(` Skipped functions/${key}.toml (active version not readable)`);
|
|
5033
5424
|
continue;
|
|
5034
5425
|
}
|
|
5035
|
-
|
|
5426
|
+
// #3455 / CR3455-001 — the generated schema copy is ONE file the whole
|
|
5427
|
+
// tree resolves through, so this plan does not decide it. Every pulled
|
|
5428
|
+
// version's answer is collected and the copy is settled once, after the
|
|
5429
|
+
// loop; otherwise the last function applied would delete or overwrite the
|
|
5430
|
+
// copy an earlier one needs.
|
|
5431
|
+
const plan = planFunctionPull({
|
|
5432
|
+
configDir,
|
|
5433
|
+
functionKey: key,
|
|
5434
|
+
envelope,
|
|
5435
|
+
includeDocumentSchema: false,
|
|
5436
|
+
});
|
|
5437
|
+
pulledSchemas.push({
|
|
5438
|
+
functionKey: key,
|
|
5439
|
+
documentSchema: typeof envelope?.documentSchema === "string" && envelope.documentSchema
|
|
5440
|
+
? envelope.documentSchema
|
|
5441
|
+
: null,
|
|
5442
|
+
});
|
|
5036
5443
|
for (const refusal of plan.refusals)
|
|
5037
5444
|
logger(` ${refusal}`);
|
|
5445
|
+
// #3482 — the retired runtime keys this pull removed on the way out. Not
|
|
5446
|
+
// a refusal: the file IS written, with every other byte unchanged, and
|
|
5447
|
+
// this is what says so rather than editing a developer's file in silence.
|
|
5448
|
+
for (const note of plan.notes)
|
|
5449
|
+
logger(` ${note}`);
|
|
5038
5450
|
const applied = applyFunctionPull(plan);
|
|
5039
5451
|
for (const refusal of applied.refusals)
|
|
5040
5452
|
logger(` ${refusal}`);
|
|
@@ -5048,11 +5460,34 @@ export async function pullFunctions(client, appId, configDir, logger = () => { }
|
|
|
5048
5460
|
functionEntities[key] = entity;
|
|
5049
5461
|
written += 1;
|
|
5050
5462
|
logger(` Wrote functions/${key}.toml and ${applied.writtenSources.length} source file(s)`);
|
|
5463
|
+
// #3462 — pull writes the ACTIVE version's bytes, which is right: the tree
|
|
5464
|
+
// should describe what runs. On a rolled-back function that silently
|
|
5465
|
+
// discards a newer version's code from the tree, so the line names both
|
|
5466
|
+
// versions and both remedies rather than leaving the author to discover it
|
|
5467
|
+
// at the next push.
|
|
5468
|
+
const newerLine = newerVersionLine(newest, active.configId, (value) => value);
|
|
5469
|
+
if (newerLine) {
|
|
5470
|
+
logger(` ${pullNewerVersionLine(key, String(active.configId), newest, (value) => value)}`);
|
|
5471
|
+
}
|
|
5051
5472
|
}
|
|
5052
5473
|
const serverKeys = items
|
|
5053
5474
|
.filter((f) => String(f?.status ?? "active") !== "archived")
|
|
5054
5475
|
.map((f) => f?.functionKey)
|
|
5055
5476
|
.filter(Boolean);
|
|
5477
|
+
// #3455 / CR3455-001 — the tree's one generated schema copy, settled from
|
|
5478
|
+
// every version this pull read. Removing it is only safe when the pull read
|
|
5479
|
+
// them ALL: a selector, or a function whose version could not be fetched,
|
|
5480
|
+
// leaves a version whose schema this pull knows nothing about.
|
|
5481
|
+
const schemaPlan = planDocumentSchemaPull({
|
|
5482
|
+
configDir,
|
|
5483
|
+
functions: pulledSchemas,
|
|
5484
|
+
coversEveryFunction: detailsOk && serverKeys.every((k) => selects(k)),
|
|
5485
|
+
});
|
|
5486
|
+
for (const warning of schemaPlan.warnings)
|
|
5487
|
+
logger(` ${warning}`);
|
|
5488
|
+
const schemaApplied = applyFunctionPull(schemaPlan);
|
|
5489
|
+
for (const refusal of schemaApplied.refusals)
|
|
5490
|
+
logger(` ${refusal}`);
|
|
5056
5491
|
const presence = detailsOk
|
|
5057
5492
|
? { ok: true, serverKeys }
|
|
5058
5493
|
: { ok: false };
|
|
@@ -7949,14 +8384,6 @@ What a push guarantees (two stages):
|
|
|
7949
8384
|
// `syncCallable` PATCH ordering a second implementation has broken before.
|
|
7950
8385
|
// Everything else about push is unchanged.
|
|
7951
8386
|
const pushSelection = readSelection(options);
|
|
7952
|
-
if (pushSelection) {
|
|
7953
|
-
const missing = unmatchedSelectors(pushSelection, configDir);
|
|
7954
|
-
if (missing.length > 0) {
|
|
7955
|
-
error(`No local file declares: ${missing.join(", ")}. Run 'primitive config pull', or 'primitive config create <type> <key>' to create one.`);
|
|
7956
|
-
process.exit(1);
|
|
7957
|
-
}
|
|
7958
|
-
info(`Applying only: ${pushSelection.raw.join(", ")}`);
|
|
7959
|
-
}
|
|
7960
8387
|
/** Narrow a type's file list to the `--only` selection. */
|
|
7961
8388
|
const selectFiles = (label, dir, files) => filterFilesBySelection(pushSelection, label, dir, files);
|
|
7962
8389
|
/**
|
|
@@ -7990,6 +8417,29 @@ What a push guarantees (two stages):
|
|
|
7990
8417
|
entities: {},
|
|
7991
8418
|
};
|
|
7992
8419
|
}
|
|
8420
|
+
// A `--only` selector that would apply nothing is refused before push
|
|
8421
|
+
// runs, so `--only workflow/typo` fails loudly instead of iterating empty
|
|
8422
|
+
// lists and reporting success.
|
|
8423
|
+
//
|
|
8424
|
+
// Checked HERE, after the baseline is loaded and after the cross-app reset
|
|
8425
|
+
// above, rather than beside `readSelection` where it used to sit (#3398).
|
|
8426
|
+
// `--prune` acts on keys the sync state tracks and NO local file declares,
|
|
8427
|
+
// so a file-only check refused the one command that is supposed to run with
|
|
8428
|
+
// the file gone: `--only workflow/household-forget --prune` reported `No
|
|
8429
|
+
// local file declares: workflow/household-forget` and left no way to retire
|
|
8430
|
+
// one tracked key without restoring its TOML (which prune would then
|
|
8431
|
+
// decline to consider) or dropping `--only` and pushing the whole tree.
|
|
8432
|
+
// The state has to be in hand to tell that key from a typo, and the
|
|
8433
|
+
// cross-app reset has to have happened first: it clears the entity ids, so
|
|
8434
|
+
// there is nothing for prune to retire and a removed key IS a typo again.
|
|
8435
|
+
if (pushSelection) {
|
|
8436
|
+
const missing = unmatchedSelectors(pushSelection, configDir, trackedSelectorKeys(syncState, { prune: !!options.prune }));
|
|
8437
|
+
if (missing.length > 0) {
|
|
8438
|
+
error(`No local file declares: ${missing.join(", ")}. Run 'primitive config pull', or 'primitive config create <type> <key>' to create one.`);
|
|
8439
|
+
process.exit(1);
|
|
8440
|
+
}
|
|
8441
|
+
info(`Applying only: ${pushSelection.raw.join(", ")}`);
|
|
8442
|
+
}
|
|
7993
8443
|
info(`Pushing configuration for app ${resolvedAppId}...`);
|
|
7994
8444
|
// The state this push STARTS from, so the failure path below can tell a
|
|
7995
8445
|
// run that recorded something from one that recorded nothing (#2936).
|
|
@@ -8377,297 +8827,25 @@ What a push guarantees (two stages):
|
|
|
8377
8827
|
pushTestCaseLookupMaps.configIdToName.set(id, scoped.slice(scoped.indexOf("#") + 1));
|
|
8378
8828
|
}
|
|
8379
8829
|
};
|
|
8380
|
-
//
|
|
8381
|
-
//
|
|
8382
|
-
//
|
|
8383
|
-
//
|
|
8384
|
-
//
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
//
|
|
8395
|
-
//
|
|
8396
|
-
|
|
8397
|
-
// Validate all workflow TOMLs (#685 misnested-header check and the
|
|
8398
|
-
// #1447 system + accessRule lint).
|
|
8399
|
-
const preflightWorkflowsDir = join(configDir, "workflows");
|
|
8400
|
-
if (existsSync(preflightWorkflowsDir)) {
|
|
8401
|
-
// Under `--only` the preflight validates just the selected files:
|
|
8402
|
-
// an unrelated broken workflow elsewhere in the slot must not block
|
|
8403
|
-
// applying the one object the operator asked for.
|
|
8404
|
-
for (const file of selectFiles("workflow", preflightWorkflowsDir, readdirSync(preflightWorkflowsDir).filter((f) => f.endsWith(".toml")))) {
|
|
8405
|
-
const filePath = join(preflightWorkflowsDir, file);
|
|
8406
|
-
try {
|
|
8407
|
-
const tomlData = parseTomlFile(filePath);
|
|
8408
|
-
// #1660 — skip platform-owned workflows (the reserved
|
|
8409
|
-
// `__internal.*` namespace) BEFORE validating, mirroring the
|
|
8410
|
-
// apply loop below. `config push` validates every workflow TOML
|
|
8411
|
-
// here, up-front, before reaching that skip — so without this
|
|
8412
|
-
// second skip a stale/hollow `__internal.*.toml` with an invalid
|
|
8413
|
-
// `steps` shape would abort the whole push in preflight. Resolve
|
|
8414
|
-
// the key the same way the apply loop does (`[workflow].key`,
|
|
8415
|
-
// falling back to the filename) and match on the resolved key,
|
|
8416
|
-
// never the filename alone, so a renamed file is still caught.
|
|
8417
|
-
const platformOwnedKey = tomlData.workflow?.key || basename(file, ".toml");
|
|
8418
|
-
if (isPlatformOwnedWorkflowKey(platformOwnedKey)) {
|
|
8419
|
-
continue;
|
|
8420
|
-
}
|
|
8421
|
-
const tomlErrors = validateWorkflowToml(tomlData);
|
|
8422
|
-
if (tomlErrors.length > 0) {
|
|
8423
|
-
preflightValidationErrors.push(formatWorkflowTomlErrors(filePath, tomlErrors));
|
|
8424
|
-
}
|
|
8425
|
-
// #2643 — non-blocking notes (e.g. `strict` / `strictParams` are
|
|
8426
|
-
// now no-ops). Printed once per file at pre-flight so the author
|
|
8427
|
-
// sees them whether or not the push goes on to apply.
|
|
8428
|
-
for (const note of collectWorkflowTomlNotes(tomlData)) {
|
|
8429
|
-
console.log(` ${filePath}: ${note}`);
|
|
8430
|
-
}
|
|
8431
|
-
const unknownKeyError = workflowUnknownKeyError(filePath, tomlData);
|
|
8432
|
-
if (unknownKeyError) {
|
|
8433
|
-
preflightValidationErrors.push(` ${unknownKeyError}`);
|
|
8434
|
-
}
|
|
8435
|
-
// #2743 — the always-send update payload cannot carry a missing
|
|
8436
|
-
// `[workflow].name`. That is a local TOML error like every other
|
|
8437
|
-
// one here, so it belongs in the front-loaded pass (#976): caught
|
|
8438
|
-
// in the apply loop it would abort AFTER earlier resources (app
|
|
8439
|
-
// settings, cron triggers, …) had already been written, and
|
|
8440
|
-
// `--dry-run` — which never reaches the apply — would plan an
|
|
8441
|
-
// update the real push then refuses.
|
|
8442
|
-
//
|
|
8443
|
-
// Scoped to the workflows this push will actually UPDATE. A
|
|
8444
|
-
// create falls back to the manifest key, so an absent name is
|
|
8445
|
-
// legitimate authoring there — and once that create lands, the
|
|
8446
|
-
// same unchanged file must keep pushing clean (the apply loop
|
|
8447
|
-
// skips it on its content hash) instead of failing a push that
|
|
8448
|
-
// was never going to send it.
|
|
8449
|
-
if (workflowPushSendsUpdate(configDir, tomlData.workflow?.key || basename(file, ".toml"), tomlData, syncState, options.force)) {
|
|
8450
|
-
const nameError = workflowUpdateNameError(filePath, tomlData.workflow || {});
|
|
8451
|
-
if (nameError) {
|
|
8452
|
-
preflightValidationErrors.push(` ${nameError}`);
|
|
8453
|
-
}
|
|
8454
|
-
}
|
|
8455
|
-
}
|
|
8456
|
-
catch (err) {
|
|
8457
|
-
preflightValidationErrors.push(` ${file}: ${err?.message || String(err)}`);
|
|
8458
|
-
}
|
|
8459
|
-
}
|
|
8460
|
-
}
|
|
8461
|
-
// Validate all database-type TOMLs (issue #803 subscription
|
|
8462
|
-
// access/accessRule conflict + issue #752 $params references).
|
|
8463
|
-
const preflightDbTypesDir = join(configDir, "database-type-configs");
|
|
8464
|
-
if (existsSync(preflightDbTypesDir)) {
|
|
8465
|
-
for (const file of selectFiles("database-type-config", preflightDbTypesDir, readdirSync(preflightDbTypesDir).filter((f) => f.endsWith(".toml")))) {
|
|
8466
|
-
const filePath = join(preflightDbTypesDir, file);
|
|
8467
|
-
let rawToml;
|
|
8468
|
-
let tomlData;
|
|
8469
|
-
try {
|
|
8470
|
-
rawToml = readFileSync(filePath, "utf-8");
|
|
8471
|
-
tomlData = parseTomlFile(filePath);
|
|
8472
|
-
}
|
|
8473
|
-
catch (err) {
|
|
8474
|
-
preflightValidationErrors.push(` ${file}: ${err?.message || String(err)}`);
|
|
8475
|
-
continue;
|
|
8476
|
-
}
|
|
8477
|
-
let operations = [];
|
|
8478
|
-
try {
|
|
8479
|
-
({ operations } = parseDatabaseTypeToml(tomlData));
|
|
8480
|
-
}
|
|
8481
|
-
catch (err) {
|
|
8482
|
-
if (err instanceof SubscriptionAccessKeyConflictError) {
|
|
8483
|
-
preflightValidationErrors.push(` ${file}: ${err.message}`);
|
|
8484
|
-
continue;
|
|
8485
|
-
}
|
|
8486
|
-
preflightValidationErrors.push(` ${file}: ${err?.message || String(err)}`);
|
|
8487
|
-
continue;
|
|
8488
|
-
}
|
|
8489
|
-
const validation = validateOperations({
|
|
8490
|
-
filePath,
|
|
8491
|
-
rawToml,
|
|
8492
|
-
operations,
|
|
8493
|
-
});
|
|
8494
|
-
for (const e of validation.errors) {
|
|
8495
|
-
preflightValidationErrors.push(` ${formatIssue(e)}`);
|
|
8496
|
-
}
|
|
8497
|
-
// Issue #1422: non-fatal deprecation notice. A `celContextAccess`
|
|
8498
|
-
// / `metadataAccess` gate on a database type config is a single CEL
|
|
8499
|
-
// expression that gates BOTH read and update of the database CEL
|
|
8500
|
-
// context — granting read implicitly grants update. Steer operators
|
|
8501
|
-
// to resource metadata categories (separate readRule/writeRule).
|
|
8502
|
-
// This is advisory only: the push continues and the runtime gate is
|
|
8503
|
-
// unchanged. It fires only when the deprecated key is actually
|
|
8504
|
-
// present, so metadata-category-only configs never trip it.
|
|
8505
|
-
const typeSection = tomlData.type || {};
|
|
8506
|
-
if (typeSection.celContextAccess || typeSection.metadataAccess) {
|
|
8507
|
-
const usedKey = typeSection.celContextAccess
|
|
8508
|
-
? "celContextAccess"
|
|
8509
|
-
: "metadataAccess";
|
|
8510
|
-
warn(` ${file}: \`${usedKey}\` is deprecated — it gates database CEL context READ and WRITE with one expression (read implies update). Prefer a resource metadata category with separate readRule/writeRule (issue #1420). Sync will continue; the runtime gate is unchanged.`);
|
|
8511
|
-
}
|
|
8512
|
-
}
|
|
8513
|
-
}
|
|
8514
|
-
// Validate app.toml settings up-front (issue #1033). A mistyped value
|
|
8515
|
-
// is a hard error that must abort BEFORE any mutation — same
|
|
8516
|
-
// fail-before-mutate contract as the vars preflight below. The apply
|
|
8517
|
-
// block re-parses for the payload.
|
|
8518
|
-
// #2645 — bounded by `--only`, like the workflow and database-type
|
|
8519
|
-
// preflights above. `config push --only workflow/x` is the replacement for
|
|
8520
|
-
// `workflows update --from-file`, so it must work on a tree where
|
|
8521
|
-
// app.toml happens to be mid-edit.
|
|
8522
|
-
const preflightAppTomlPath = join(configDir, "app.toml");
|
|
8523
|
-
if (selectionIncludesApp(pushSelection) && existsSync(preflightAppTomlPath)) {
|
|
8524
|
-
try {
|
|
8525
|
-
const parsedApp = parseTomlFile(preflightAppTomlPath);
|
|
8526
|
-
const appErrors = collectAppSettingsPushErrors(parsedApp, {
|
|
8527
|
-
// #2880 DSO-001 — the same exclusion the apply block computes, so
|
|
8528
|
-
// the preflight validates the body that will actually be sent.
|
|
8529
|
-
excludeName: appSettingsNameExcluded({
|
|
8530
|
-
isCrossAppPush: !!isCrossAppPush,
|
|
8531
|
-
recordedExcludedName: syncState?.entities?.app?.excludedName,
|
|
8532
|
-
fileName: parsedApp?.app?.name,
|
|
8533
|
-
}),
|
|
8534
|
-
});
|
|
8535
|
-
for (const e of appErrors) {
|
|
8536
|
-
preflightValidationErrors.push(` app.toml: ${e}`);
|
|
8537
|
-
}
|
|
8538
|
-
}
|
|
8539
|
-
catch (err) {
|
|
8540
|
-
preflightValidationErrors.push(` app.toml: ${err?.message || String(err)}`);
|
|
8541
|
-
}
|
|
8542
|
-
}
|
|
8543
|
-
// Validate config vars up-front (issue #1423 review). Parse vars.toml
|
|
8544
|
-
// and check the same key/value constraints the server enforces here, in
|
|
8545
|
-
// the preflight pass, so an invalid entry aborts BEFORE any mutation
|
|
8546
|
-
// (app settings or a var upsert) is applied. Previously validation ran
|
|
8547
|
-
// mid-push, after app settings could already have been updated.
|
|
8548
|
-
// #2645 — bounded by `--only`, for the same reason as app.toml above.
|
|
8830
|
+
// #3375 — the front-loaded pass now lives in one exported entry point,
|
|
8831
|
+
// `runConfigPushPreflight`, so a rule declared `preflight` can be
|
|
8832
|
+
// proved to be reached from it before anything below mutates. Nothing
|
|
8833
|
+
// about what it checks, the order it checks it, or what it prints
|
|
8834
|
+
// moved with it.
|
|
8835
|
+
const { parsedVars: preflightParsedVars } = await runConfigPushPreflight({
|
|
8836
|
+
configDir,
|
|
8837
|
+
resolvedAppId,
|
|
8838
|
+
pushSelection,
|
|
8839
|
+
syncState,
|
|
8840
|
+
force: !!options.force,
|
|
8841
|
+
isCrossAppPush: !!isCrossAppPush,
|
|
8842
|
+
client,
|
|
8843
|
+
});
|
|
8844
|
+
// Re-derived, not returned: both are one-line functions of inputs the
|
|
8845
|
+
// entry point already took, and widening its return type to carry them
|
|
8846
|
+
// would make the vars apply leg depend on the preflight's internals.
|
|
8549
8847
|
const preflightVarsTomlPath = join(configDir, "vars.toml");
|
|
8550
8848
|
const varsSelected = selectionIncludesAnyVar(pushSelection);
|
|
8551
|
-
let preflightParsedVars = null;
|
|
8552
|
-
if (varsSelected && existsSync(preflightVarsTomlPath)) {
|
|
8553
|
-
try {
|
|
8554
|
-
const parsed = parseTomlFile(preflightVarsTomlPath);
|
|
8555
|
-
// #2645 — under `--only var/<key>` only the selected entries are
|
|
8556
|
-
// checked. The rest of the file is not being written, and aborting
|
|
8557
|
-
// over an entry this push will not touch is the same over-reach as
|
|
8558
|
-
// running the leg at all for an unselected singleton.
|
|
8559
|
-
for (const e of validateVarsFile(parsed, {
|
|
8560
|
-
onlyKeys: pushSelection ? pushSelection.varKeys : undefined,
|
|
8561
|
-
})) {
|
|
8562
|
-
preflightValidationErrors.push(` ${e}`);
|
|
8563
|
-
}
|
|
8564
|
-
preflightParsedVars = parsed;
|
|
8565
|
-
}
|
|
8566
|
-
catch (err) {
|
|
8567
|
-
preflightValidationErrors.push(` vars.toml: ${err?.message || String(err)}`);
|
|
8568
|
-
}
|
|
8569
|
-
}
|
|
8570
|
-
// #3182 — a function's capabilities, checked against the author's own
|
|
8571
|
-
// file before anything is applied. The grammar is purely local (and
|
|
8572
|
-
// since #3279 it keeps three kinds: `integration:<key>`,
|
|
8573
|
-
// `secret:<NAME>` and the high-blast opt-ins — a retired grant string
|
|
8574
|
-
// is refused here with the retirement); whether an `integration:`
|
|
8575
|
-
// names an integration that will exist is answered against the
|
|
8576
|
-
// tree's own `integrations/` (applied BEFORE functions) merged with
|
|
8577
|
-
// the integrations the server already has. If the live listing cannot
|
|
8578
|
-
// be fetched and the tree declares none, the target check is skipped
|
|
8579
|
-
// and the server — which is authoritative — has the last word.
|
|
8580
|
-
const preflightFunctionsDir = join(configDir, "functions");
|
|
8581
|
-
if (existsSync(preflightFunctionsDir)) {
|
|
8582
|
-
const functionFiles = selectFiles("function", preflightFunctionsDir, readdirSync(preflightFunctionsDir).filter((f) => f.endsWith(".toml")));
|
|
8583
|
-
if (functionFiles.length > 0) {
|
|
8584
|
-
const treeTargets = configObjectKeysFromTree(configDir);
|
|
8585
|
-
let knownTargets = null;
|
|
8586
|
-
try {
|
|
8587
|
-
const integrations = await client.listIntegrations(resolvedAppId, {
|
|
8588
|
-
status: "active",
|
|
8589
|
-
});
|
|
8590
|
-
knownTargets = mergeKnownGrantTargets({
|
|
8591
|
-
integrations: new Set((integrations?.items ?? [])
|
|
8592
|
-
.map((row) => String(row?.integrationKey ?? ""))
|
|
8593
|
-
.filter(Boolean)),
|
|
8594
|
-
}, treeTargets);
|
|
8595
|
-
}
|
|
8596
|
-
catch {
|
|
8597
|
-
knownTargets =
|
|
8598
|
-
treeTargets.integrations.size > 0 ? treeTargets : null;
|
|
8599
|
-
}
|
|
8600
|
-
for (const file of functionFiles) {
|
|
8601
|
-
const filePath = join(preflightFunctionsDir, file);
|
|
8602
|
-
let tomlData;
|
|
8603
|
-
try {
|
|
8604
|
-
tomlData = parseTomlFile(filePath);
|
|
8605
|
-
}
|
|
8606
|
-
catch (err) {
|
|
8607
|
-
preflightValidationErrors.push(` ${filePath}: ${err?.message || String(err)}`);
|
|
8608
|
-
continue;
|
|
8609
|
-
}
|
|
8610
|
-
for (const message of functionGrantErrors({
|
|
8611
|
-
filePath,
|
|
8612
|
-
tomlData,
|
|
8613
|
-
knownTargets,
|
|
8614
|
-
})) {
|
|
8615
|
-
preflightValidationErrors.push(` ${message}`);
|
|
8616
|
-
}
|
|
8617
|
-
// #3320 — and everything else the file decides on its own: the
|
|
8618
|
-
// `entry`, the mode the two spellings resolve to, the trigger
|
|
8619
|
-
// rules, and a webhook trigger's signing-secret requirement and
|
|
8620
|
-
// reference shape. These used to be raised inside the apply loop,
|
|
8621
|
-
// after webhooks, integrations, vars and database types had
|
|
8622
|
-
// already been written — the partial tree this issue is about.
|
|
8623
|
-
// Bounded by the same `selectFiles` above, so `--only` keeps its
|
|
8624
|
-
// posture (#2645).
|
|
8625
|
-
const functionKey = tomlData?.function?.key || basename(file, ".toml");
|
|
8626
|
-
for (const message of functionTomlPreflightErrors({
|
|
8627
|
-
filePath,
|
|
8628
|
-
functionKey,
|
|
8629
|
-
tomlData,
|
|
8630
|
-
})) {
|
|
8631
|
-
preflightValidationErrors.push(` ${filePath}: ${message}`);
|
|
8632
|
-
}
|
|
8633
|
-
}
|
|
8634
|
-
}
|
|
8635
|
-
}
|
|
8636
|
-
// #2644 — reject any key the vendored definition does not recognize,
|
|
8637
|
-
// for every migrated configuration type. Same posture the `[workflow]`
|
|
8638
|
-
// check above has, generalized: a mistyped key fails here, in the
|
|
8639
|
-
// preflight pass, so nothing is applied and no request is issued —
|
|
8640
|
-
// rather than being dropped on the way to the server and reading as a
|
|
8641
|
-
// successful push that changed nothing.
|
|
8642
|
-
// #2880 — the same collector `config diff` runs, so the two commands
|
|
8643
|
-
// cannot disagree about which files are pushable.
|
|
8644
|
-
for (const rejected of collectConfigFileValidationErrors(configDir)) {
|
|
8645
|
-
for (const message of rejected.messages) {
|
|
8646
|
-
preflightValidationErrors.push(` ${message}`);
|
|
8647
|
-
}
|
|
8648
|
-
}
|
|
8649
|
-
// The `<key>.tests/*.toml` sidecars, which sit beside the block they
|
|
8650
|
-
// test rather than in a type directory of their own. Same posture,
|
|
8651
|
-
// reached through the registry's `test-case` surface — and through the
|
|
8652
|
-
// same collector `config diff` runs per sidecar (#2880), so neither
|
|
8653
|
-
// command can refuse a file the other calls Synced.
|
|
8654
|
-
for (const filePath of testCaseTomlFiles(configDir)) {
|
|
8655
|
-
let tomlData;
|
|
8656
|
-
try {
|
|
8657
|
-
tomlData = parseTomlFile(filePath);
|
|
8658
|
-
}
|
|
8659
|
-
catch {
|
|
8660
|
-
// Reported by the per-block test-case loop with its own message.
|
|
8661
|
-
continue;
|
|
8662
|
-
}
|
|
8663
|
-
for (const message of testCaseSidecarPreflightErrors(filePath, tomlData)) {
|
|
8664
|
-
preflightValidationErrors.push(` ${message}`);
|
|
8665
|
-
}
|
|
8666
|
-
}
|
|
8667
|
-
if (preflightValidationErrors.length > 0) {
|
|
8668
|
-
throw new Error(`Aborting push: ${preflightValidationErrors.length} TOML validation error(s) — no changes were applied.\n` +
|
|
8669
|
-
preflightValidationErrors.join("\n"));
|
|
8670
|
-
}
|
|
8671
8849
|
// Fetch the current remote config vars up-front — before ANY mutation
|
|
8672
8850
|
// (issue #1423 review pass 3). `planVarsPush` needs this snapshot both
|
|
8673
8851
|
// to detect concurrent remote edits and to decide which already-absent
|
|
@@ -13128,7 +13306,7 @@ What a push guarantees (two stages):
|
|
|
13128
13306
|
}
|
|
13129
13307
|
return typecheckSetup.checker;
|
|
13130
13308
|
};
|
|
13131
|
-
const files = selectFiles("function", functionsPushDir, readdirSync(functionsPushDir).filter(
|
|
13309
|
+
const files = selectFiles("function", functionsPushDir, readdirSync(functionsPushDir).filter(isFunctionDeclarationFile));
|
|
13132
13310
|
const functionSpec = configDiffSpec("function");
|
|
13133
13311
|
const liveFunctions = new Map();
|
|
13134
13312
|
let functionsLiveOk = true;
|
|
@@ -13149,9 +13327,27 @@ What a push guarantees (two stages):
|
|
|
13149
13327
|
const liveActiveVersion = async (functionId, activeConfigId) => {
|
|
13150
13328
|
if (!activeConfigId)
|
|
13151
13329
|
return null;
|
|
13152
|
-
|
|
13153
|
-
|
|
13330
|
+
// #3462 — the point read: one call by id, and correct for a
|
|
13331
|
+
// rolled-back function whose active version is past page one.
|
|
13332
|
+
return client.getFunctionConfig(resolvedAppId, functionId, String(activeConfigId));
|
|
13154
13333
|
};
|
|
13334
|
+
// #3455 — the tree's document schema is a property of the PROJECT,
|
|
13335
|
+
// not of one function, so an unusable `models/models.toml` is said
|
|
13336
|
+
// once rather than repeated for every function in the tree.
|
|
13337
|
+
const documentSchemaResolution = resolveDocumentSchema(configDir);
|
|
13338
|
+
const documentSchemaWarning = documentSchemaResolution.warning;
|
|
13339
|
+
let documentSchemaWarningShown = false;
|
|
13340
|
+
// Say ONCE that this push drops a document schema a running
|
|
13341
|
+
// version carries — and ask the NEXT function when this one's
|
|
13342
|
+
// version had nothing to lose, so a schema-less function pushed
|
|
13343
|
+
// first cannot silence a schema-bearing sibling. The rule and its
|
|
13344
|
+
// state live in `function-sync.ts`, where they are testable
|
|
13345
|
+
// without a tree and a server.
|
|
13346
|
+
const warnIfDroppingDocumentSchema = createDocumentSchemaDropWarner({
|
|
13347
|
+
treeCarriesSchema: documentSchemaResolution.origin !== "none",
|
|
13348
|
+
readConfig: (functionId, configId) => client.getFunctionConfig(resolvedAppId, functionId, configId),
|
|
13349
|
+
warn: (message) => warn(` ${message}`),
|
|
13350
|
+
});
|
|
13155
13351
|
for (const file of files) {
|
|
13156
13352
|
const filePath = join(functionsPushDir, file);
|
|
13157
13353
|
const tomlData = parseTomlFile(filePath);
|
|
@@ -13180,6 +13376,15 @@ What a push guarantees (two stages):
|
|
|
13180
13376
|
});
|
|
13181
13377
|
continue;
|
|
13182
13378
|
}
|
|
13379
|
+
// #3455 (D3455-004) — the project declares a document schema this
|
|
13380
|
+
// push cannot carry: over the cap, or not parsing. VISIBLE rather
|
|
13381
|
+
// than refused, because a broken `models/models.toml` is not a
|
|
13382
|
+
// reason to stop deploying code — but silence would leave an
|
|
13383
|
+
// author wondering why a first write still cannot seed.
|
|
13384
|
+
if (documentSchemaWarning && !documentSchemaWarningShown) {
|
|
13385
|
+
documentSchemaWarningShown = true;
|
|
13386
|
+
warn(` ${documentSchemaWarning}`);
|
|
13387
|
+
}
|
|
13183
13388
|
// #3331 — and then the types. esbuild erases them, so a build that
|
|
13184
13389
|
// succeeded says nothing about whether the code holds against the
|
|
13185
13390
|
// declarations this same push generated: `ctx.user` going nullable
|
|
@@ -13356,6 +13561,23 @@ What a push guarantees (two stages):
|
|
|
13356
13561
|
});
|
|
13357
13562
|
continue;
|
|
13358
13563
|
}
|
|
13564
|
+
// #3455 (D3455-010) — did the server keep the document schema it
|
|
13565
|
+
// was sent? A server that predates the field drops it silently and
|
|
13566
|
+
// still answers 200, so without this the push reports success over
|
|
13567
|
+
// a version that can never seed. Recorded against THIS function and
|
|
13568
|
+
// NOT reported pushed; the rest of the tree still lands.
|
|
13569
|
+
const retention = documentSchemaRetentionFailure({
|
|
13570
|
+
sentDocumentSchema: typeof push.documentSchema === "string",
|
|
13571
|
+
builtEnvelopeHash: push.envelopeHash,
|
|
13572
|
+
storedEnvelopeHash: pushedVersion?.envelopeHash,
|
|
13573
|
+
});
|
|
13574
|
+
if (retention) {
|
|
13575
|
+
applyFailures.push({ type: "function", key, message: retention });
|
|
13576
|
+
continue;
|
|
13577
|
+
}
|
|
13578
|
+
await warnIfDroppingDocumentSchema(String(functionId), liveFunction?.activeConfigId
|
|
13579
|
+
? String(liveFunction.activeConfigId)
|
|
13580
|
+
: undefined);
|
|
13359
13581
|
if (pushedVersion?.created === false) {
|
|
13360
13582
|
info(` Function ${key}: code unchanged (no new version)`);
|
|
13361
13583
|
}
|
|
@@ -15108,7 +15330,7 @@ What a push guarantees (two stages):
|
|
|
15108
15330
|
const localFunctionsParsed = new Map();
|
|
15109
15331
|
const functionsDirPath = join(configDir, "functions");
|
|
15110
15332
|
if (functionsOutcomeDiff.ok && existsSync(functionsDirPath)) {
|
|
15111
|
-
for (const file of readdirSync(functionsDirPath).filter(
|
|
15333
|
+
for (const file of readdirSync(functionsDirPath).filter(isFunctionDeclarationFile)) {
|
|
15112
15334
|
const filePath = join(functionsDirPath, file);
|
|
15113
15335
|
const tomlData = parseTomlFile(filePath);
|
|
15114
15336
|
const key = tomlData.function?.key || basename(file, ".toml");
|
|
@@ -15402,8 +15624,14 @@ What a push guarantees (two stages):
|
|
|
15402
15624
|
const versionOutcome = await attemptFetch(async () => {
|
|
15403
15625
|
if (!remote.activeConfigId)
|
|
15404
15626
|
return null;
|
|
15405
|
-
|
|
15406
|
-
return
|
|
15627
|
+
// #3462 — the point read, for the same reason as pull's.
|
|
15628
|
+
return client.getFunctionConfig(resolvedAppId, remote.functionId, String(remote.activeConfigId));
|
|
15629
|
+
});
|
|
15630
|
+
// #3462 — and the newest version, so a tree that matches something
|
|
15631
|
+
// that EXISTS can say so. One row; the listing answers newest first.
|
|
15632
|
+
const newestOutcome = await attemptFetch(async () => {
|
|
15633
|
+
const page = await client.listFunctionConfigs(resolvedAppId, remote.functionId, { limit: 1 });
|
|
15634
|
+
return (page.items || [])[0] ?? null;
|
|
15407
15635
|
});
|
|
15408
15636
|
if (!versionOutcome.ok) {
|
|
15409
15637
|
hint = "content not compared (fetch failed)";
|
|
@@ -15427,9 +15655,22 @@ What a push guarantees (two stages):
|
|
|
15427
15655
|
if (!active ||
|
|
15428
15656
|
String(active.envelopeHash ?? "") !== built.envelopeHash) {
|
|
15429
15657
|
status = "modified";
|
|
15658
|
+
// #3462 — when the tree matches the NEWEST version rather
|
|
15659
|
+
// than the active one, the function is rolled back and the
|
|
15660
|
+
// generic hint sends an operator looking for an edit they
|
|
15661
|
+
// never made. Naming both versions says what actually
|
|
15662
|
+
// happened and what each command would do about it.
|
|
15663
|
+
const newest = newestOutcome.ok
|
|
15664
|
+
? newestOutcome.value
|
|
15665
|
+
: null;
|
|
15430
15666
|
hint =
|
|
15431
|
-
|
|
15432
|
-
|
|
15667
|
+
active &&
|
|
15668
|
+
newest &&
|
|
15669
|
+
String(newest.configId) !== String(active.configId) &&
|
|
15670
|
+
String(newest.envelopeHash ?? "") === built.envelopeHash
|
|
15671
|
+
? rollbackDiffHint(String(newest.configId), String(active.configId))
|
|
15672
|
+
: "the built bundle or a source file differs — `config push` " +
|
|
15673
|
+
"creates a new immutable version";
|
|
15433
15674
|
}
|
|
15434
15675
|
}
|
|
15435
15676
|
catch (err) {
|
|
@@ -15922,6 +16163,13 @@ What a push guarantees (two stages):
|
|
|
15922
16163
|
// calls Synced, Modified or "local only — new".
|
|
15923
16164
|
// Merged first: a file both collectors reject keeps every message push
|
|
15924
16165
|
// prints, rather than one collector's hint overwriting the other's.
|
|
16166
|
+
// #3482 — the same advisory `config push` prints, so an author sees
|
|
16167
|
+
// the retired key before they push rather than only during it. Not a
|
|
16168
|
+
// row status: the file is pushable, and saying otherwise would make
|
|
16169
|
+
// `config diff` disagree with the push it predicts.
|
|
16170
|
+
for (const message of collectFunctionTomlIgnoredKeyWarnings(configDir)) {
|
|
16171
|
+
warn(` ${message}`);
|
|
16172
|
+
}
|
|
15925
16173
|
for (const rejected of mergeConfigFileValidationErrors([
|
|
15926
16174
|
...collectConfigFileValidationErrors(configDir),
|
|
15927
16175
|
...collectFunctionTomlValidationErrors(configDir),
|