@void/deploy-cloudflare 0.21.2 → 0.21.4
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/index.d.mts +10 -2
- package/dist/index.mjs +104 -35
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -69,6 +69,7 @@ export declare function assertCloudflareDurableObjectLifecycleSupported(options:
|
|
|
69
69
|
accountId: string;
|
|
70
70
|
apiBaseUrl: string;
|
|
71
71
|
readMigrationTag?: (options: CloudflareWorkerMigrationTagOptions) => Promise<string | null>;
|
|
72
|
+
allowPendingAtomicMigration?: boolean;
|
|
72
73
|
}): Promise<void>;
|
|
73
74
|
export declare function assertCloudflareBindingsPreserved(before: ExistingCloudflareWorker, after: ExistingCloudflareWorker, approvedKv?: ReadonlyArray<ApprovedCloudflareKvBinding>, options?: {
|
|
74
75
|
staticAssetsBinding?: string;
|
|
@@ -197,7 +198,7 @@ type CloudflareVersionActivation = {
|
|
|
197
198
|
trafficConfig?: CloudflareTrafficConfig;
|
|
198
199
|
} | {
|
|
199
200
|
ok: false;
|
|
200
|
-
reason: "uploader-changed" | "status-failed" | "deployment-changed" | "inheritance-source-unavailable" | "inheritance-source-changed" | "traffic-preservation-unavailable" | "traffic-split-unsupported" | "upload-failed" | "upload-output-invalid" | "preview-unavailable" | "readiness-staging-failed" | "readiness-failed" | "readiness-access-denied" | "bootstrap-failed" | "bootstrap-output-invalid" | "bootstrap-readiness-failed" | "bootstrap-readiness-access-denied" | "activation-failed" | "triggers-failed";
|
|
201
|
+
reason: "uploader-changed" | "status-failed" | "deployment-changed" | "inheritance-source-unavailable" | "inheritance-source-changed" | "traffic-preservation-unavailable" | "traffic-split-unsupported" | "upload-failed" | "upload-output-invalid" | "preview-unavailable" | "readiness-staging-failed" | "readiness-failed" | "readiness-access-denied" | "bootstrap-failed" | "bootstrap-output-invalid" | "bootstrap-readiness-failed" | "bootstrap-readiness-access-denied" | "atomic-failed" | "atomic-output-invalid" | "atomic-readiness-failed" | "atomic-readiness-access-denied" | "activation-failed" | "triggers-failed";
|
|
201
202
|
status?: number;
|
|
202
203
|
cloudflareErrorCode?: number;
|
|
203
204
|
/** Bounded uploader error text with terminal escapes removed and common credential formats redacted. */
|
|
@@ -273,7 +274,7 @@ type CloudflareVersionActivationOptions = {
|
|
|
273
274
|
preserveTriggers?: boolean;
|
|
274
275
|
expectedPreviousVersionId?: string;
|
|
275
276
|
trustedInheritanceVersionIds?: ReadonlyArray<string>;
|
|
276
|
-
recordVerifiedCandidate?: (versionId: string) => void | Promise<void>;
|
|
277
|
+
recordVerifiedCandidate?: (versionId: string, parentVersionId: string | null) => void | Promise<void>;
|
|
277
278
|
validateUploadedVersion?: (versionId: string) => Promise<void>;
|
|
278
279
|
runWrangler: CloudflareCommandRunner;
|
|
279
280
|
verifyUploader: () => boolean;
|
|
@@ -309,6 +310,13 @@ type CloudflareVersionActivationOptions = {
|
|
|
309
310
|
readinessAttempts?: number;
|
|
310
311
|
/** Verify that omitted remote secrets come from the active Worker Version before uploading. */
|
|
311
312
|
verifyInheritedSecrets?: boolean;
|
|
313
|
+
/** Pin opaque secret bindings to the live version when an unpublished upload is newer. */
|
|
314
|
+
pinActiveSecrets?: (activeVersionId: string, bindings: ReadonlyArray<{
|
|
315
|
+
name: string;
|
|
316
|
+
type: "secret_text" | "secret_key";
|
|
317
|
+
}>) => Promise<boolean> | boolean;
|
|
318
|
+
/** Explicitly publish an existing Durable Object Worker in one atomic operation. */
|
|
319
|
+
atomic?: boolean;
|
|
312
320
|
/**
|
|
313
321
|
* Plaintext secrets to attach only to the uploaded candidate version. Supplying every remote
|
|
314
322
|
* `secret_text` binding lets a deployment recover safely when an inactive upload is newer than
|
package/dist/index.mjs
CHANGED
|
@@ -246,13 +246,15 @@ async function assertCloudflareDurableObjectLifecycleSupported(options) {
|
|
|
246
246
|
const afterTag = await readMigrationTag(migrationOptions);
|
|
247
247
|
if (!after || after.snapshot.versionId !== existing.snapshot.versionId || afterTag !== activeTag) throw new Error("Cloudflare production or Durable Object migration state changed during verification. Retry from the current deployment.");
|
|
248
248
|
if (activeTag === null && existing.isSecretOnlyDraft && after.isSecretOnlyDraft) return;
|
|
249
|
+
if (activeTag === null && declarativeClassesMatch(migrations, existing.snapshot.runtime.exports)) return;
|
|
249
250
|
const activeIndex = activeTag === null ? -1 : migrations.findIndex((migration) => migration.tag === activeTag);
|
|
250
251
|
const pending = migrations.slice(activeIndex + 1).filter((migration) => migration.changesClasses);
|
|
251
252
|
if (pending.length === 0) return;
|
|
253
|
+
if (options.allowPendingAtomicMigration === true) return;
|
|
252
254
|
throw new Error([
|
|
253
|
-
"deploy:
|
|
255
|
+
"deploy: this Durable Object class migration needs an atomic Cloudflare deployment.",
|
|
254
256
|
`Pending migration tag${pending.length === 1 ? "" : "s"}: ${pending.map((migration) => migration.tag).join(", ")}.`,
|
|
255
|
-
"
|
|
257
|
+
"Review the migration, then run `void deploy --platform cloudflare --atomic`. The Worker will receive traffic before readiness is checked.",
|
|
256
258
|
"No database migration or Worker secret was changed."
|
|
257
259
|
].join("\n"));
|
|
258
260
|
}
|
|
@@ -270,10 +272,39 @@ function parseDurableObjectMigrations(value) {
|
|
|
270
272
|
for (const field of fields) if (field !== void 0 && !Array.isArray(field)) throw new Error("deploy: generated Cloudflare config has a malformed class migration.");
|
|
271
273
|
return {
|
|
272
274
|
tag: migration.tag,
|
|
273
|
-
changesClasses: fields.some((field) => Array.isArray(field) && field.length > 0)
|
|
275
|
+
changesClasses: fields.some((field) => Array.isArray(field) && field.length > 0),
|
|
276
|
+
step: migration
|
|
274
277
|
};
|
|
275
278
|
});
|
|
276
279
|
}
|
|
280
|
+
/** Cloudflare's declarative exports replace the legacy migration tag after an atomic publish. */
|
|
281
|
+
function declarativeClassesMatch(migrations, actualValue) {
|
|
282
|
+
if (!record$1(actualValue)) return false;
|
|
283
|
+
const desired = /* @__PURE__ */ new Map();
|
|
284
|
+
for (const { step } of migrations) {
|
|
285
|
+
for (const [field, storage] of [["new_classes", "legacy-kv"], ["new_sqlite_classes", "sqlite"]]) for (const name of step[field] ?? []) {
|
|
286
|
+
if (typeof name !== "string") return false;
|
|
287
|
+
desired.set(name, storage);
|
|
288
|
+
}
|
|
289
|
+
for (const item of step.renamed_classes ?? []) {
|
|
290
|
+
if (!record$1(item) || typeof item.from !== "string" || typeof item.to !== "string") return false;
|
|
291
|
+
const storage = desired.get(item.from);
|
|
292
|
+
if (storage !== "sqlite" && storage !== "legacy-kv") return false;
|
|
293
|
+
desired.set(item.from, "deleted");
|
|
294
|
+
desired.set(item.to, storage);
|
|
295
|
+
}
|
|
296
|
+
for (const name of step.deleted_classes ?? []) {
|
|
297
|
+
if (typeof name !== "string") return false;
|
|
298
|
+
desired.set(name, "deleted");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (desired.size === 0) return false;
|
|
302
|
+
return [...desired].every(([name, storage]) => {
|
|
303
|
+
const current = actualValue[name];
|
|
304
|
+
if (storage === "deleted") return current === void 0 || record$1(current) && current.state === "deleted";
|
|
305
|
+
return record$1(current) && current.type === "durable-object" && current.storage === storage;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
277
308
|
function isMissingWorkerOutput$1(output) {
|
|
278
309
|
return /\[code:\s*10007\]/i.test(output) || /no deployments|\bworker\b.*\b(?:does not exist|not found)\b/i.test(output);
|
|
279
310
|
}
|
|
@@ -1144,6 +1175,7 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1144
1175
|
};
|
|
1145
1176
|
const previousVersionId = previous.versionId;
|
|
1146
1177
|
let inheritanceBaseline;
|
|
1178
|
+
let trustedCandidateChain = false;
|
|
1147
1179
|
if (options.expectedPreviousVersionId !== void 0 || options.verifyInheritedSecrets === true) {
|
|
1148
1180
|
const recent = await readRecentCloudflareVersionIds(options, commonArgs);
|
|
1149
1181
|
if (recent === null) return {
|
|
@@ -1151,29 +1183,33 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1151
1183
|
reason: "inheritance-source-unavailable"
|
|
1152
1184
|
};
|
|
1153
1185
|
const latestVersionId = recent.at(-1)?.id ?? null;
|
|
1154
|
-
if (options.expectedPreviousVersionId !== void 0) {
|
|
1186
|
+
if (options.expectedPreviousVersionId !== void 0 || options.trustedInheritanceVersionIds !== void 0) {
|
|
1155
1187
|
const activeIndex = recent.findIndex((version) => version.id === previous.versionId);
|
|
1156
1188
|
const inheritedChain = activeIndex < 0 ? [] : recent.slice(activeIndex);
|
|
1157
1189
|
const inheritedCandidates = inheritedChain.slice(1).map((version) => version.id);
|
|
1158
|
-
|
|
1190
|
+
trustedCandidateChain = options.trustedInheritanceVersionIds !== void 0 && options.trustedInheritanceVersionIds.length > 0 && arraysEqual(inheritedCandidates, options.trustedInheritanceVersionIds);
|
|
1191
|
+
if (!(inheritedChain.length > 0 && inheritedChain.every((version, index) => index === 0 || version.number === inheritedChain[index - 1].number + 1) && (arraysEqual(inheritedCandidates, options.trustedInheritanceVersionIds ?? []) || options.pinActiveSecrets !== void 0))) return {
|
|
1159
1192
|
ok: false,
|
|
1160
1193
|
reason: "inheritance-source-changed",
|
|
1161
1194
|
activeVersionId: recent.at(-1)?.id ?? null,
|
|
1162
1195
|
rollback: "not-needed"
|
|
1163
1196
|
};
|
|
1164
1197
|
}
|
|
1165
|
-
if (options.verifyInheritedSecrets === true && previous.versionId !== latestVersionId) {
|
|
1198
|
+
if (options.verifyInheritedSecrets === true && previous.versionId !== latestVersionId && !trustedCandidateChain) {
|
|
1166
1199
|
const replacesAllRemoteSecrets = await candidateReplacesAllRemoteSecrets(options, commonArgs, previous.versionId, latestVersionId);
|
|
1167
1200
|
if (replacesAllRemoteSecrets === null) return {
|
|
1168
1201
|
ok: false,
|
|
1169
1202
|
reason: "inheritance-source-unavailable"
|
|
1170
1203
|
};
|
|
1171
|
-
if (!replacesAllRemoteSecrets)
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1204
|
+
if (!replacesAllRemoteSecrets) {
|
|
1205
|
+
const activeSecrets = previous.versionId ? await readCloudflareVersionSecretBindings(options, commonArgs, previous.versionId) : null;
|
|
1206
|
+
if (!activeSecrets || !options.pinActiveSecrets || !await options.pinActiveSecrets(previous.versionId, activeSecrets)) return {
|
|
1207
|
+
ok: false,
|
|
1208
|
+
reason: "inheritance-source-changed",
|
|
1209
|
+
activeVersionId: latestVersionId,
|
|
1210
|
+
rollback: "not-needed"
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1177
1213
|
}
|
|
1178
1214
|
inheritanceBaseline = recent;
|
|
1179
1215
|
}
|
|
@@ -1214,6 +1250,10 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1214
1250
|
} : versionedPreviousTriggerConfig;
|
|
1215
1251
|
const previousTriggerConfig = triggerConfigWithQueueConsumerCleanup(versionedPreviousTriggerConfigWithTraffic, options.queueConsumerCleanup);
|
|
1216
1252
|
const alias = `void-ready-${Date.now().toString(36)}`;
|
|
1253
|
+
if (options.atomic && previous.versionId !== null) return bootstrapFirstDeployment({
|
|
1254
|
+
...options,
|
|
1255
|
+
message
|
|
1256
|
+
}, commonArgs, previous.versionId, true);
|
|
1217
1257
|
options.onProgress?.("Uploading the Worker and assets...");
|
|
1218
1258
|
const upload = await Promise.resolve(options.runWrangler([
|
|
1219
1259
|
"versions",
|
|
@@ -1278,8 +1318,8 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1278
1318
|
}
|
|
1279
1319
|
await options.validateUploadedVersion?.(versionId);
|
|
1280
1320
|
if (options.recordVerifiedCandidate) {
|
|
1281
|
-
if (!options.validateUploadedVersion) throw new Error("A Worker candidate cannot be recorded without validating its upload.");
|
|
1282
|
-
await options.recordVerifiedCandidate(versionId);
|
|
1321
|
+
if (!options.validateUploadedVersion && !inheritanceBaseline) throw new Error("A Worker candidate cannot be recorded without validating its upload and version sequence.");
|
|
1322
|
+
await options.recordVerifiedCandidate(versionId, previous.versionId);
|
|
1283
1323
|
}
|
|
1284
1324
|
const reportedPreviewUrl = output.match(PREVIEW_URL_RE)?.[1];
|
|
1285
1325
|
let authenticatedWorkersDevSubdomain = reportedPreviewUrl ? extractWorkersDevSubdomain(reportedPreviewUrl) : void 0;
|
|
@@ -1311,14 +1351,19 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1311
1351
|
const stagingOwnerFailure = await verifyReadinessStagingOwner(options, commonArgs, previous.versionId, versionId);
|
|
1312
1352
|
if (stagingOwnerFailure) return stagingOwnerFailure;
|
|
1313
1353
|
const staged = await deployVersionPercentages(options, commonArgs, [`${previous.versionId}@100`, `${versionId}@0`], message);
|
|
1314
|
-
if (!isCleanCommandExit(staged))
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1354
|
+
if (!isCleanCommandExit(staged)) {
|
|
1355
|
+
const cloudflareErrorCode = parseCloudflareErrorCode(`${staged.stdout}\n${staged.stderr}`);
|
|
1356
|
+
return {
|
|
1357
|
+
ok: false,
|
|
1358
|
+
reason: "readiness-staging-failed",
|
|
1359
|
+
status: staged.status,
|
|
1360
|
+
...commandDiagnosticFields(staged),
|
|
1361
|
+
...cloudflareErrorCode === void 0 ? {} : { cloudflareErrorCode },
|
|
1362
|
+
versionId,
|
|
1363
|
+
previousVersionId: previous.versionId,
|
|
1364
|
+
rollback: "not-needed"
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1322
1367
|
stagedVersionOverride = true;
|
|
1323
1368
|
}
|
|
1324
1369
|
options.onProgress?.("Waiting for the Worker to become ready...");
|
|
@@ -1336,14 +1381,19 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1336
1381
|
const stagingOwnerFailure = await verifyReadinessStagingOwner(options, commonArgs, previous.versionId, versionId);
|
|
1337
1382
|
if (stagingOwnerFailure) return stagingOwnerFailure;
|
|
1338
1383
|
const staged = await deployVersionPercentages(options, commonArgs, [`${previous.versionId}@100`, `${versionId}@0`], message);
|
|
1339
|
-
if (!isCleanCommandExit(staged))
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1384
|
+
if (!isCleanCommandExit(staged)) {
|
|
1385
|
+
const cloudflareErrorCode = parseCloudflareErrorCode(`${staged.stdout}\n${staged.stderr}`);
|
|
1386
|
+
return {
|
|
1387
|
+
ok: false,
|
|
1388
|
+
reason: "readiness-staging-failed",
|
|
1389
|
+
status: staged.status,
|
|
1390
|
+
...commandDiagnosticFields(staged),
|
|
1391
|
+
...cloudflareErrorCode === void 0 ? {} : { cloudflareErrorCode },
|
|
1392
|
+
versionId,
|
|
1393
|
+
previousVersionId: previous.versionId,
|
|
1394
|
+
rollback: "not-needed"
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1347
1397
|
readinessUrl = stableReadinessUrl;
|
|
1348
1398
|
authenticatedWorkersDevSubdomain = extractWorkersDevSubdomain(stableReadinessUrl);
|
|
1349
1399
|
readinessUsesCustomRoute = !desiredTriggerConfig.workers_dev;
|
|
@@ -1680,12 +1730,26 @@ async function readCloudflareVersionSecretBindings(options, commonArgs, versionI
|
|
|
1680
1730
|
* brand-new Worker can therefore need one ordinary deploy before later releases use preview-first
|
|
1681
1731
|
* version activation. This fallback requires prior proof that no application had been deployed.
|
|
1682
1732
|
*/
|
|
1683
|
-
async function bootstrapFirstDeployment(options, commonArgs, previousVersionId) {
|
|
1733
|
+
async function bootstrapFirstDeployment(options, commonArgs, previousVersionId, atomic = false) {
|
|
1684
1734
|
if (!options.verifyUploader()) return {
|
|
1685
1735
|
ok: false,
|
|
1686
1736
|
reason: "uploader-changed"
|
|
1687
1737
|
};
|
|
1688
|
-
|
|
1738
|
+
if (atomic) {
|
|
1739
|
+
const current = await readCurrentProductionVersion(options, commonArgs);
|
|
1740
|
+
if (current.kind === "failed") return {
|
|
1741
|
+
ok: false,
|
|
1742
|
+
reason: "status-failed",
|
|
1743
|
+
status: current.status
|
|
1744
|
+
};
|
|
1745
|
+
if (current.kind !== "ok" || current.versionId !== previousVersionId) return {
|
|
1746
|
+
ok: false,
|
|
1747
|
+
reason: "deployment-changed",
|
|
1748
|
+
activeVersionId: current.kind === "ok" ? current.versionId : null,
|
|
1749
|
+
rollback: "not-needed"
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
options.onProgress?.(atomic ? "Publishing the Worker atomically..." : "Publishing the first Worker deployment...");
|
|
1689
1753
|
const bootstrap = await Promise.resolve(options.runWrangler([
|
|
1690
1754
|
"deploy",
|
|
1691
1755
|
...commonArgs,
|
|
@@ -1705,7 +1769,7 @@ async function bootstrapFirstDeployment(options, commonArgs, previousVersionId)
|
|
|
1705
1769
|
const cloudflareErrorCode = parseCloudflareErrorCode(output);
|
|
1706
1770
|
return {
|
|
1707
1771
|
ok: false,
|
|
1708
|
-
reason: "bootstrap-failed",
|
|
1772
|
+
reason: atomic ? "atomic-failed" : "bootstrap-failed",
|
|
1709
1773
|
status: bootstrap.status,
|
|
1710
1774
|
...commandDiagnosticFields(bootstrap),
|
|
1711
1775
|
...cloudflareErrorCode === void 0 ? {} : { cloudflareErrorCode },
|
|
@@ -1719,7 +1783,7 @@ async function bootstrapFirstDeployment(options, commonArgs, previousVersionId)
|
|
|
1719
1783
|
const reportedOrConfiguredUrl = reportedUrl ?? (desiredTriggerConfig.workers_dev ? workersDevReadinessUrl(options.workerName, options.workersDevSubdomain) : customRouteReadinessUrl(desiredTriggerConfig.routes));
|
|
1720
1784
|
if (!versionId || !reportedOrConfiguredUrl) return {
|
|
1721
1785
|
ok: false,
|
|
1722
|
-
reason: "bootstrap-output-invalid",
|
|
1786
|
+
reason: atomic ? "atomic-output-invalid" : "bootstrap-output-invalid",
|
|
1723
1787
|
previousVersionId,
|
|
1724
1788
|
...versionId ? { versionId } : {}
|
|
1725
1789
|
};
|
|
@@ -1728,7 +1792,7 @@ async function bootstrapFirstDeployment(options, commonArgs, previousVersionId)
|
|
|
1728
1792
|
const ready = await probeReadiness(`${previewUrl.replace(/\/$/, "")}/__void/ready`, options.workerName, reportedUrl || desiredTriggerConfig.workers_dev ? options.readinessHeaders : void 0, extractWorkersDevSubdomain(previewUrl), void 0, options.fetch ?? globalThis.fetch, options.sleep ?? defaultSleep, options.readinessAttempts ?? 30, 6e4);
|
|
1729
1793
|
if (ready !== "ready") return {
|
|
1730
1794
|
ok: false,
|
|
1731
|
-
reason: ready === "access-denied" ? "bootstrap-readiness-access-denied" : "bootstrap-readiness-failed",
|
|
1795
|
+
reason: ready === "access-denied" ? atomic ? "atomic-readiness-access-denied" : "bootstrap-readiness-access-denied" : atomic ? "atomic-readiness-failed" : "bootstrap-readiness-failed",
|
|
1732
1796
|
versionId,
|
|
1733
1797
|
previousVersionId,
|
|
1734
1798
|
rollback: "unavailable"
|
|
@@ -1738,6 +1802,11 @@ async function bootstrapFirstDeployment(options, commonArgs, previousVersionId)
|
|
|
1738
1802
|
versionId,
|
|
1739
1803
|
previewUrl,
|
|
1740
1804
|
previousVersionId,
|
|
1805
|
+
...atomic ? { trafficConfig: {
|
|
1806
|
+
workers_dev: desiredTriggerConfig.workers_dev,
|
|
1807
|
+
preview_urls: desiredTriggerConfig.preview_urls,
|
|
1808
|
+
routes: desiredTriggerConfig.routes
|
|
1809
|
+
} } : {},
|
|
1741
1810
|
...workersDevSubdomainResult(previewUrl)
|
|
1742
1811
|
};
|
|
1743
1812
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void/deploy-cloudflare",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.4",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/voidzero-dev/void.git",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@void/deploy-core": "0.21.
|
|
23
|
+
"@void/deploy-core": "0.21.4",
|
|
24
24
|
"jsonc-parser": "3.3.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|