@hs-x/cli 0.2.4 → 0.2.5
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/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/kit.d.ts.map +1 -1
- package/dist/cli/kit.js +1 -1
- package/dist/cli/kit.js.map +1 -1
- package/dist/cloudflare-oauth.d.ts.map +1 -1
- package/dist/cloudflare-oauth.js +10 -9
- package/dist/cloudflare-oauth.js.map +1 -1
- package/dist/commands/connect.d.ts +2 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +29 -1
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/control-plane-read.d.ts +60 -9
- package/dist/commands/control-plane-read.d.ts.map +1 -1
- package/dist/commands/control-plane-read.js +219 -26
- package/dist/commands/control-plane-read.js.map +1 -1
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +311 -17
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +7 -4
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +33 -11
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/migrate.d.ts +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +119 -11
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/status.d.ts +7 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +45 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dev/invoke.d.ts +9 -0
- package/dist/dev/invoke.d.ts.map +1 -1
- package/dist/dev/invoke.js +67 -0
- package/dist/dev/invoke.js.map +1 -1
- package/dist/errors-registry.d.ts.map +1 -1
- package/dist/errors-registry.js +29 -0
- package/dist/errors-registry.js.map +1 -1
- package/dist/project-binding.d.ts +7 -1
- package/dist/project-binding.d.ts.map +1 -1
- package/dist/project-binding.js +10 -5
- package/dist/project-binding.js.map +1 -1
- package/package.json +7 -7
package/dist/commands/deploy.js
CHANGED
|
@@ -412,6 +412,11 @@ export async function deployCommand({ argv, root, json, }) {
|
|
|
412
412
|
projectId = answer;
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
|
+
if (projectId && accountId && !resolvedAcct.overrodeBinding) {
|
|
416
|
+
// Sticky binding, project half: project-scoped reads (status/logs/routes/
|
|
417
|
+
// drift) resolve their project id from here when no flag is passed.
|
|
418
|
+
await writeProjectBinding(root, accountId, projectId).catch(() => { });
|
|
419
|
+
}
|
|
415
420
|
if (!json && willChangeAnything && !flagYes && isInteractive()) {
|
|
416
421
|
const summary = buildDeployPlanSummary({
|
|
417
422
|
accountId,
|
|
@@ -437,13 +442,31 @@ export async function deployCommand({ argv, root, json, }) {
|
|
|
437
442
|
return cancelledResult(argv, 'deploy');
|
|
438
443
|
}
|
|
439
444
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
445
|
+
// The schema apply is the first phase that touches the portal. A failure here
|
|
446
|
+
// used to throw a raw HubSpot error out of the command — no HSX_E code, no
|
|
447
|
+
// phase/PLAN output (cold-stranger run #3, finding A). Render the curated
|
|
448
|
+
// failure WITH the plan that was being applied instead.
|
|
449
|
+
let portalSchemaApply;
|
|
450
|
+
if (applySchema) {
|
|
451
|
+
try {
|
|
452
|
+
portalSchemaApply = await applyPortalSchemaPlan({
|
|
453
|
+
argv,
|
|
454
|
+
plan: portalSchemaPlan,
|
|
455
|
+
source: portalSchemaRead?.source,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
return portalSchemaApplyFailure({
|
|
460
|
+
argv,
|
|
461
|
+
json,
|
|
462
|
+
root,
|
|
463
|
+
startedAt: commandStartedAt,
|
|
464
|
+
workers,
|
|
465
|
+
portalSchemaPlan,
|
|
466
|
+
error,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
447
470
|
const manifest = await generateProjectArtifacts({
|
|
448
471
|
root,
|
|
449
472
|
workers,
|
|
@@ -906,6 +929,11 @@ function renderDeployHuman(input) {
|
|
|
906
929
|
.map((worker) => `${worker.workerName}${worker.dryRun ? ' (dry-run)' : ''}`)
|
|
907
930
|
.join(', ');
|
|
908
931
|
reporter.info(`Cloudflare deploy: ${deployed}`);
|
|
932
|
+
for (const worker of input.cloudflareDeployResult.workers) {
|
|
933
|
+
if (worker.unauthenticatedInvoke) {
|
|
934
|
+
reporter.warn('HSX_W_DEPLOY_UNAUTHENTICATED_INVOKE', `${worker.workerName}: workflow-action invoke endpoints accept unsigned requests until the app's OAuth client secret is configured (redeploy with HSX_HUBSPOT_CLIENT_SECRET / --hubspot-client-secret after the OAuth install).`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
909
937
|
}
|
|
910
938
|
if (!input.linked && input.cloudflareDeployResult) {
|
|
911
939
|
reporter.info(`Deploy history for this machine: ${machineDashboardUrl(input.machineId)}`);
|
|
@@ -927,6 +955,22 @@ function renderDeployHuman(input) {
|
|
|
927
955
|
if (upload.deployId !== undefined) {
|
|
928
956
|
reporter.info(`HubSpot deploy id: ${upload.deployId}`);
|
|
929
957
|
}
|
|
958
|
+
else if (hubspotBuildStatus === 'SUCCESS' &&
|
|
959
|
+
upload.deploySkipped &&
|
|
960
|
+
upload.deployStatus !== undefined) {
|
|
961
|
+
const autoStatus = readStatus(upload.deployStatus) ?? 'UNKNOWN';
|
|
962
|
+
if (autoStatus === 'SUCCESS') {
|
|
963
|
+
reporter.info(`HubSpot deploy: auto-deploy SUCCESS (build #${upload.buildId})`);
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
reporter.warn('HSX_W_DEPLOY_HUBSPOT_AUTODEPLOY_FAILED', `HubSpot auto-deploy finished ${autoStatus} — the build is green but the app did NOT ship. Check the deploy log in the HubSpot UI (project ${upload.projectName}).`);
|
|
967
|
+
for (const subbuild of readSubbuildStatuses(upload.deployStatus)) {
|
|
968
|
+
if (subbuild.status !== 'SUCCESS') {
|
|
969
|
+
reporter.info(` [x] ${subbuild.name}: ${subbuild.errorMessage ?? subbuild.status}`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
930
974
|
else if (hubspotBuildStatus === 'SUCCESS' && upload.deploySkipped) {
|
|
931
975
|
reporter.info('HubSpot deploy: skipped (upload-only or auto-deploy handles it)');
|
|
932
976
|
}
|
|
@@ -1077,13 +1121,24 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
|
|
|
1077
1121
|
});
|
|
1078
1122
|
const applied = [];
|
|
1079
1123
|
const skipped = [];
|
|
1124
|
+
// Each HubSpot write is wrapped so a failure carries WHICH action failed, the
|
|
1125
|
+
// target object type (scope errors are per-object), and what already applied
|
|
1126
|
+
// before it — the curated failure renderer needs all three.
|
|
1127
|
+
const apply = async (label, objectType, run) => {
|
|
1128
|
+
try {
|
|
1129
|
+
await run();
|
|
1130
|
+
}
|
|
1131
|
+
catch (cause) {
|
|
1132
|
+
throw new PortalSchemaApplyError({ action: label, objectType, applied: [...applied], cause });
|
|
1133
|
+
}
|
|
1134
|
+
applied.push(label);
|
|
1135
|
+
};
|
|
1080
1136
|
for (const action of plan.actions) {
|
|
1081
1137
|
if (action.kind === 'will-create-object') {
|
|
1082
|
-
await client.schemaManagement.createSchema({
|
|
1138
|
+
await apply(`create-object:${action.objectType}`, action.objectType, () => client.schemaManagement.createSchema({
|
|
1083
1139
|
name: action.objectType,
|
|
1084
1140
|
labels: portalObjectLabels(action.objectType),
|
|
1085
|
-
});
|
|
1086
|
-
applied.push(`create-object:${action.objectType}`);
|
|
1141
|
+
}));
|
|
1087
1142
|
continue;
|
|
1088
1143
|
}
|
|
1089
1144
|
if (!action.propertyName || !action.declaredType) {
|
|
@@ -1092,25 +1147,23 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
|
|
|
1092
1147
|
}
|
|
1093
1148
|
if (action.kind === 'will-create-property') {
|
|
1094
1149
|
const fieldType = defaultHubSpotFieldType(action.declaredType);
|
|
1095
|
-
await client.schemaManagement.createProperty({
|
|
1150
|
+
await apply(`create-property:${action.objectType}.${action.propertyName}`, action.objectType, () => client.schemaManagement.createProperty({
|
|
1096
1151
|
objectType: action.objectType,
|
|
1097
1152
|
name: action.propertyName,
|
|
1098
1153
|
label: propertyLabel(action.propertyName),
|
|
1099
1154
|
type: action.declaredType,
|
|
1100
1155
|
...(fieldType ? { fieldType } : {}),
|
|
1101
|
-
});
|
|
1102
|
-
applied.push(`create-property:${action.objectType}.${action.propertyName}`);
|
|
1156
|
+
}));
|
|
1103
1157
|
continue;
|
|
1104
1158
|
}
|
|
1105
1159
|
if (action.kind === 'will-alter-property') {
|
|
1106
1160
|
const fieldType = defaultHubSpotFieldType(action.declaredType);
|
|
1107
|
-
await client.schemaManagement.updateProperty({
|
|
1161
|
+
await apply(`alter-property:${action.objectType}.${action.propertyName}`, action.objectType, () => client.schemaManagement.updateProperty({
|
|
1108
1162
|
objectType: action.objectType,
|
|
1109
1163
|
propertyName: action.propertyName,
|
|
1110
1164
|
type: action.declaredType,
|
|
1111
1165
|
...(fieldType ? { fieldType } : {}),
|
|
1112
|
-
});
|
|
1113
|
-
applied.push(`alter-property:${action.objectType}.${action.propertyName}`);
|
|
1166
|
+
}));
|
|
1114
1167
|
}
|
|
1115
1168
|
}
|
|
1116
1169
|
return {
|
|
@@ -1119,6 +1172,198 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
|
|
|
1119
1172
|
skipped,
|
|
1120
1173
|
};
|
|
1121
1174
|
}
|
|
1175
|
+
/** A portal-schema apply action failed against HubSpot. */
|
|
1176
|
+
class PortalSchemaApplyError extends Error {
|
|
1177
|
+
action;
|
|
1178
|
+
objectType;
|
|
1179
|
+
/** Actions that DID apply before the failure (partial-apply visibility). */
|
|
1180
|
+
applied;
|
|
1181
|
+
cause;
|
|
1182
|
+
constructor(input) {
|
|
1183
|
+
super(input.cause instanceof Error ? input.cause.message : String(input.cause));
|
|
1184
|
+
this.name = 'PortalSchemaApplyError';
|
|
1185
|
+
this.action = input.action;
|
|
1186
|
+
this.objectType = input.objectType;
|
|
1187
|
+
this.applied = input.applied;
|
|
1188
|
+
this.cause = input.cause;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* The raw HubSpot response body off a failed schema call. local-dev-lib wraps
|
|
1193
|
+
* axios failures in `HubSpotHttpError` and lifts `response.data` to `.data`;
|
|
1194
|
+
* a raw axios error keeps it at `.cause.response.data`.
|
|
1195
|
+
*/
|
|
1196
|
+
function hubSpotErrorBody(error) {
|
|
1197
|
+
if (!isRecord(error))
|
|
1198
|
+
return undefined;
|
|
1199
|
+
if (error.data !== undefined)
|
|
1200
|
+
return error.data;
|
|
1201
|
+
const cause = error.cause;
|
|
1202
|
+
if (isRecord(cause) && isRecord(cause.response))
|
|
1203
|
+
return cause.response.data;
|
|
1204
|
+
return undefined;
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Extract HubSpot's MISSING_SCOPES payload. Captured live (cold-stranger run #3
|
|
1208
|
+
* repro — POST crm/v3/properties/contacts with a PAK lacking the write scope):
|
|
1209
|
+
*
|
|
1210
|
+
* { "status": "error",
|
|
1211
|
+
* "message": "This app hasn't been granted all required scopes to make this call. …",
|
|
1212
|
+
* "category": "MISSING_SCOPES",
|
|
1213
|
+
* "correlationId": "…",
|
|
1214
|
+
* "errors": [{ "message": "One or more of the following scopes are required.",
|
|
1215
|
+
* "context": { "requiredGranularScopes": ["crm.schemas.contacts.write", …] } }] }
|
|
1216
|
+
*
|
|
1217
|
+
* The list is ANY-OF (any one scope grants the call). Older error shapes use
|
|
1218
|
+
* `requiredScopes` (the only key local-dev-lib knows — which is why its own
|
|
1219
|
+
* message renders an EMPTY scope list for granular-scope errors).
|
|
1220
|
+
* Returns undefined when the error is not a scope error.
|
|
1221
|
+
*/
|
|
1222
|
+
function hubSpotMissingScopes(error) {
|
|
1223
|
+
const body = hubSpotErrorBody(error);
|
|
1224
|
+
if (!isRecord(body))
|
|
1225
|
+
return undefined;
|
|
1226
|
+
const scopes = [];
|
|
1227
|
+
if (Array.isArray(body.errors)) {
|
|
1228
|
+
for (const entry of body.errors) {
|
|
1229
|
+
if (!isRecord(entry) || !isRecord(entry.context))
|
|
1230
|
+
continue;
|
|
1231
|
+
for (const key of ['requiredGranularScopes', 'requiredScopes']) {
|
|
1232
|
+
const list = entry.context[key];
|
|
1233
|
+
if (!Array.isArray(list))
|
|
1234
|
+
continue;
|
|
1235
|
+
for (const scope of list) {
|
|
1236
|
+
if (typeof scope === 'string')
|
|
1237
|
+
scopes.push(scope);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (scopes.length === 0 && body.category !== 'MISSING_SCOPES')
|
|
1243
|
+
return undefined;
|
|
1244
|
+
return {
|
|
1245
|
+
scopes: [...new Set(scopes)],
|
|
1246
|
+
...(typeof body.correlationId === 'string' ? { correlationId: body.correlationId } : {}),
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
// Object types with a dedicated property-settings write scope
|
|
1250
|
+
// (crm.schemas.<type>.write — "create, delete, or make changes to property
|
|
1251
|
+
// settings"; crm.objects.<type>.write is RECORD writes and does not grant
|
|
1252
|
+
// property creation). Everything else falls under crm.schemas.custom.write.
|
|
1253
|
+
const HUBSPOT_SCHEMA_SCOPE_OBJECTS = new Set([
|
|
1254
|
+
'appointments',
|
|
1255
|
+
'carts',
|
|
1256
|
+
'commercepayments',
|
|
1257
|
+
'companies',
|
|
1258
|
+
'contacts',
|
|
1259
|
+
'courses',
|
|
1260
|
+
'deals',
|
|
1261
|
+
'invoices',
|
|
1262
|
+
'line_items',
|
|
1263
|
+
'listings',
|
|
1264
|
+
'orders',
|
|
1265
|
+
'quotes',
|
|
1266
|
+
'services',
|
|
1267
|
+
'subscriptions',
|
|
1268
|
+
'tickets',
|
|
1269
|
+
'users',
|
|
1270
|
+
]);
|
|
1271
|
+
/** The property-definition write scope `--apply-schema` needs for an object type. */
|
|
1272
|
+
function propertyWriteScopeFor(objectType) {
|
|
1273
|
+
return HUBSPOT_SCHEMA_SCOPE_OBJECTS.has(objectType)
|
|
1274
|
+
? `crm.schemas.${objectType}.write`
|
|
1275
|
+
: 'crm.schemas.custom.write';
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Narrow HubSpot's any-of scope list to what actually unblocks THIS apply:
|
|
1279
|
+
* the schemas-write scope for the target object when present, else every scope
|
|
1280
|
+
* that names the object, else the full response list (else the doc-grounded
|
|
1281
|
+
* default for the object type when the response carried none).
|
|
1282
|
+
*/
|
|
1283
|
+
function relevantMissingScopes(all, objectType) {
|
|
1284
|
+
const expected = propertyWriteScopeFor(objectType);
|
|
1285
|
+
if (all.includes(expected))
|
|
1286
|
+
return [expected];
|
|
1287
|
+
const mentioning = all.filter((scope) => scope.includes(`.${objectType}.`));
|
|
1288
|
+
if (mentioning.length > 0)
|
|
1289
|
+
return mentioning;
|
|
1290
|
+
return all.length > 0 ? all : [expected];
|
|
1291
|
+
}
|
|
1292
|
+
const HUBSPOT_PAK_URL = 'https://app.hubspot.com/l/personal-access-key';
|
|
1293
|
+
/**
|
|
1294
|
+
* Curated rendering for a failed `--apply-schema` leg (cold-stranger run #3,
|
|
1295
|
+
* finding A): the phase + PLAN output still prints, partial progress is shown,
|
|
1296
|
+
* and a HubSpot MISSING_SCOPES response becomes HSX_E_DEPLOY_SCHEMA_SCOPES
|
|
1297
|
+
* naming the exact scopes to grant — never a raw truncated HubSpot error.
|
|
1298
|
+
*/
|
|
1299
|
+
function portalSchemaApplyFailure(input) {
|
|
1300
|
+
const applyError = input.error instanceof PortalSchemaApplyError ? input.error : undefined;
|
|
1301
|
+
const cause = applyError?.cause ?? input.error;
|
|
1302
|
+
const scopeError = hubSpotMissingScopes(cause);
|
|
1303
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
1304
|
+
const hubSpotDetail = `${causeMessage.split('\n')[0]}${scopeError?.correlationId ? ` (correlationId ${scopeError.correlationId})` : ''}`;
|
|
1305
|
+
let code;
|
|
1306
|
+
let message;
|
|
1307
|
+
let hint;
|
|
1308
|
+
let missingScopes = [];
|
|
1309
|
+
if (scopeError && applyError) {
|
|
1310
|
+
code = 'HSX_E_DEPLOY_SCHEMA_SCOPES';
|
|
1311
|
+
missingScopes = relevantMissingScopes(scopeError.scopes, applyError.objectType);
|
|
1312
|
+
const scopesText = missingScopes.length === 1
|
|
1313
|
+
? `the ${missingScopes[0]} scope`
|
|
1314
|
+
: `one of these scopes: ${missingScopes.join(', ')}`;
|
|
1315
|
+
message = `HubSpot rejected ${applyError.action}: your personal access key cannot write ${applyError.objectType} property definitions. Grant ${scopesText}.`;
|
|
1316
|
+
hint = `Edit your personal access key at ${HUBSPOT_PAK_URL} and add ${missingScopes.length === 1 ? missingScopes[0] : 'one of the scopes above'}, then re-run. To preview without applying: hs-x deploy --plan --portal-schema-live.`;
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
code = 'HSX_E_DEPLOY_SCHEMA_APPLY';
|
|
1320
|
+
message = applyError
|
|
1321
|
+
? `Portal schema apply failed at ${applyError.action}: ${causeMessage.split('\n')[0]}`
|
|
1322
|
+
: causeMessage;
|
|
1323
|
+
hint = 'Preview the schema plan without applying: hs-x deploy --plan --portal-schema-live.';
|
|
1324
|
+
}
|
|
1325
|
+
const exitCode = code === 'HSX_E_DEPLOY_SCHEMA_SCOPES' ? 10 : 1;
|
|
1326
|
+
if (input.json) {
|
|
1327
|
+
write(`${JSON.stringify({
|
|
1328
|
+
schema_version: 1,
|
|
1329
|
+
command: 'deploy',
|
|
1330
|
+
ok: false,
|
|
1331
|
+
...(input.portalSchemaPlan ? { portalSchemaPlan: input.portalSchemaPlan } : {}),
|
|
1332
|
+
...(applyError && applyError.applied.length > 0
|
|
1333
|
+
? { portalSchemaApplied: applyError.applied }
|
|
1334
|
+
: {}),
|
|
1335
|
+
error: {
|
|
1336
|
+
code,
|
|
1337
|
+
message,
|
|
1338
|
+
hint,
|
|
1339
|
+
...(missingScopes.length > 0 ? { missing_scopes: missingScopes } : {}),
|
|
1340
|
+
detail: hubSpotDetail,
|
|
1341
|
+
},
|
|
1342
|
+
}, null, 2)}\n`);
|
|
1343
|
+
return { exitCode };
|
|
1344
|
+
}
|
|
1345
|
+
const reporter = createReporter({ command: 'deploy', argv: input.argv, startedAt: input.startedAt });
|
|
1346
|
+
reporter.header(basename(input.root));
|
|
1347
|
+
const capCount = input.workers.reduce((count, worker) => count + worker.capabilities.length, 0);
|
|
1348
|
+
reporter
|
|
1349
|
+
.step('Validating project')
|
|
1350
|
+
.ok(`${input.workers.length} workers, ${capCount} capabilities`);
|
|
1351
|
+
if (input.portalSchemaPlan) {
|
|
1352
|
+
reporter.info(renderPortalSchemaPlan(input.portalSchemaPlan).trimEnd());
|
|
1353
|
+
}
|
|
1354
|
+
if (applyError && applyError.applied.length > 0) {
|
|
1355
|
+
for (const done of applyError.applied)
|
|
1356
|
+
reporter.info(`APPLIED ${done}`);
|
|
1357
|
+
}
|
|
1358
|
+
reporter.step('Applying portal schema').fail(applyError?.action);
|
|
1359
|
+
reporter.error(code, message, {
|
|
1360
|
+
hint,
|
|
1361
|
+
cause: hubSpotDetail,
|
|
1362
|
+
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
1363
|
+
});
|
|
1364
|
+
reporter.done(undefined, exitCode);
|
|
1365
|
+
return { exitCode };
|
|
1366
|
+
}
|
|
1122
1367
|
function renderPortalSchemaPlan(plan) {
|
|
1123
1368
|
if (plan.items.length === 0) {
|
|
1124
1369
|
return 'Portal schema: no changes or warnings.\n';
|
|
@@ -1520,6 +1765,10 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, source
|
|
|
1520
1765
|
process.stderr.write(`warning: could not register the tenant Worker URL with the control plane: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1521
1766
|
}
|
|
1522
1767
|
}
|
|
1768
|
+
// Without the client secret on the Worker, the runtime accepts UNSIGNED
|
|
1769
|
+
// workflow-action invokes (HubSpot must still be able to call them pre-OAuth)
|
|
1770
|
+
// — surface that as a one-line warning instead of leaving it silent.
|
|
1771
|
+
const hasWorkflowActions = worker.capabilities.some((capability) => capability.kind === 'tool');
|
|
1523
1772
|
return {
|
|
1524
1773
|
workerName,
|
|
1525
1774
|
workerSourcePath: relative(root, workerSourcePath),
|
|
@@ -1530,6 +1779,9 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, source
|
|
|
1530
1779
|
dryRun,
|
|
1531
1780
|
stdout: output.stdout,
|
|
1532
1781
|
stderr: output.stderr,
|
|
1782
|
+
...(hasWorkflowActions && !resolvedHubSpotClientSecret && !dryRun
|
|
1783
|
+
? { unauthenticatedInvoke: true }
|
|
1784
|
+
: {}),
|
|
1533
1785
|
};
|
|
1534
1786
|
}
|
|
1535
1787
|
async function discoverWorkerSourcePaths(root) {
|
|
@@ -2351,6 +2603,14 @@ async function executeHubSpotOnlyUpload({ argv, root, local, uploadOnly, }) {
|
|
|
2351
2603
|
: 0;
|
|
2352
2604
|
if (appId)
|
|
2353
2605
|
await persistHubSpotAppBinding(root, { appId, projectName, developerAccountId });
|
|
2606
|
+
// Build SUCCESS does NOT mean the app shipped: HubSpot's auto-deploy can
|
|
2607
|
+
// still fail (found live: "Apps are not allowed to have 0 redirect URLs"
|
|
2608
|
+
// failed deploy #4 while the CLI reported green). Verify the deploy too.
|
|
2609
|
+
// Auto-deploy fires on uploads too — verify it regardless of uploadOnly
|
|
2610
|
+
// (the f1-data 0-redirect failure rode exactly this path).
|
|
2611
|
+
const autoDeployStatus = autoDeployEnabled && readStatus(buildStatus) === 'SUCCESS'
|
|
2612
|
+
? await waitForHubSpotAutoDeploy({ client, projectName, buildStatus })
|
|
2613
|
+
: undefined;
|
|
2354
2614
|
return {
|
|
2355
2615
|
projectName,
|
|
2356
2616
|
archivePath,
|
|
@@ -2360,7 +2620,7 @@ async function executeHubSpotOnlyUpload({ argv, root, local, uploadOnly, }) {
|
|
|
2360
2620
|
buildId: upload.buildId,
|
|
2361
2621
|
deployId: undefined,
|
|
2362
2622
|
buildStatus,
|
|
2363
|
-
deployStatus:
|
|
2623
|
+
deployStatus: autoDeployStatus,
|
|
2364
2624
|
deploySkipped: Boolean(uploadOnly || autoDeployEnabled),
|
|
2365
2625
|
local,
|
|
2366
2626
|
};
|
|
@@ -2401,6 +2661,40 @@ async function optionalHubSpotAppId(client, projectName, appUid) {
|
|
|
2401
2661
|
return 0;
|
|
2402
2662
|
}
|
|
2403
2663
|
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Poll the auto-deploy triggered by a successful build until it reaches a
|
|
2666
|
+
* terminal state. The build status carries the deploy task locator
|
|
2667
|
+
* (deployStatusTaskLocator.id); absent/transient lookups resolve undefined —
|
|
2668
|
+
* the caller treats that as "could not verify", never as success.
|
|
2669
|
+
*/
|
|
2670
|
+
async function waitForHubSpotAutoDeploy({ client, projectName, buildStatus, timeoutMs = 60_000, intervalMs = 2_000, }) {
|
|
2671
|
+
const locator = buildStatus && typeof buildStatus === 'object'
|
|
2672
|
+
? buildStatus.deployStatusTaskLocator
|
|
2673
|
+
: undefined;
|
|
2674
|
+
const deployId = locator && typeof locator.id === 'number'
|
|
2675
|
+
? locator.id
|
|
2676
|
+
: locator && typeof locator.id === 'string'
|
|
2677
|
+
? Number.parseInt(locator.id, 10)
|
|
2678
|
+
: undefined;
|
|
2679
|
+
if (deployId === undefined || Number.isNaN(deployId))
|
|
2680
|
+
return undefined;
|
|
2681
|
+
const deadline = Date.now() + timeoutMs;
|
|
2682
|
+
let last;
|
|
2683
|
+
while (Date.now() < deadline) {
|
|
2684
|
+
try {
|
|
2685
|
+
last = await client.projects.getDeployStatus({ projectName, deployId });
|
|
2686
|
+
}
|
|
2687
|
+
catch {
|
|
2688
|
+
last = undefined;
|
|
2689
|
+
}
|
|
2690
|
+
const status = readStatus(last);
|
|
2691
|
+
if (status && status !== 'PENDING' && status !== 'BUILDING' && status !== 'DEPLOYING') {
|
|
2692
|
+
return last;
|
|
2693
|
+
}
|
|
2694
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
2695
|
+
}
|
|
2696
|
+
return last;
|
|
2697
|
+
}
|
|
2404
2698
|
async function waitForHubSpotBuildStatus({ client, projectName, buildId, timeoutMs, intervalMs, }) {
|
|
2405
2699
|
const startedAt = Date.now();
|
|
2406
2700
|
let latest;
|