@hs-x/cli 0.2.4 → 0.2.6
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-auth.d.ts.map +1 -1
- package/dist/cloudflare-auth.js +7 -0
- package/dist/cloudflare-auth.js.map +1 -1
- package/dist/cloudflare-oauth.d.ts.map +1 -1
- package/dist/cloudflare-oauth.js +11 -10
- package/dist/cloudflare-oauth.js.map +1 -1
- package/dist/commands/account.d.ts +6 -6
- package/dist/commands/api.d.ts +1 -1
- package/dist/commands/connect.d.ts +2 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +103 -178
- 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 +222 -26
- package/dist/commands/control-plane-read.js.map +1 -1
- package/dist/commands/deploy-promote.d.ts +1 -1
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +326 -18
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +22 -6
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/flags.d.ts +1 -1
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +37 -16
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/link.d.ts +1 -1
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +83 -0
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/login.d.ts +2 -2
- package/dist/commands/migrate.d.ts +17 -17
- 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/rollback.d.ts +1 -1
- package/dist/commands/secrets.d.ts +2 -2
- 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/commands/unlink.d.ts +1 -1
- package/dist/commands/validate.d.ts +2 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dev/invoke.d.ts +26 -0
- package/dist/dev/invoke.d.ts.map +1 -1
- package/dist/dev/invoke.js +155 -7
- package/dist/dev/invoke.js.map +1 -1
- package/dist/errors-registry.d.ts.map +1 -1
- package/dist/errors-registry.js +33 -4
- 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/dist/services/live.d.ts +1 -1
- package/package.json +9 -7
package/dist/commands/deploy.js
CHANGED
|
@@ -351,7 +351,7 @@ export async function deployCommand({ argv, root, json, }) {
|
|
|
351
351
|
const applySchema = argv.includes('--apply-schema');
|
|
352
352
|
const heartbeatFlag = argv.includes('--heartbeat');
|
|
353
353
|
const noHeartbeat = argv.includes('--no-heartbeat');
|
|
354
|
-
const
|
|
354
|
+
const noRecord = argv.includes('--no-record');
|
|
355
355
|
const explicitControlPlaneUrl = resolveFlag(argv, '--control-plane-url');
|
|
356
356
|
const controlPlaneUrl = explicitControlPlaneUrl ?? (planOnly ? undefined : process.env.HSX_CONTROL_PLANE_URL);
|
|
357
357
|
const linked = await isLinked();
|
|
@@ -389,6 +389,20 @@ export async function deployCommand({ argv, root, json, }) {
|
|
|
389
389
|
const cloudflareDeployRequested = !buildOnly &&
|
|
390
390
|
(explicitCloudflareDeployRequested || (!planOnly && needsRuntime && !explicitHubspotUpload));
|
|
391
391
|
const hubspotUploadRequested = !buildOnly && (explicitHubspotUpload || (!planOnly && !cloudflareDryRun && Boolean(projectId)));
|
|
392
|
+
// Hosted control-plane deploys record by default: shipping code without
|
|
393
|
+
// recording the revision guarantees `unknown_code` drift on the next
|
|
394
|
+
// attestation (the worker attests a deploy id the control plane has no
|
|
395
|
+
// record of) and `hs-x list` keeps saying "no recorded deploys".
|
|
396
|
+
// `--no-record` opts out; `--record-local` / `--promote-when-healthy`
|
|
397
|
+
// force recording regardless (incl. against `--local-control-plane`).
|
|
398
|
+
const shouldRecordControlPlane = recordLocal ||
|
|
399
|
+
promoteWhenHealthy ||
|
|
400
|
+
(!noRecord &&
|
|
401
|
+
!planOnly &&
|
|
402
|
+
!buildOnly &&
|
|
403
|
+
!cloudflareDryRun &&
|
|
404
|
+
Boolean(controlPlaneUrl) &&
|
|
405
|
+
(cloudflareDeployRequested || hubspotUploadRequested));
|
|
392
406
|
const portalSchemaRead = await readPortalSchema(argv, workers);
|
|
393
407
|
const portalSchemaPlan = portalSchemaRead
|
|
394
408
|
? planPortalSchemaManagement({ workers, observed: portalSchemaRead.observed })
|
|
@@ -412,6 +426,11 @@ export async function deployCommand({ argv, root, json, }) {
|
|
|
412
426
|
projectId = answer;
|
|
413
427
|
}
|
|
414
428
|
}
|
|
429
|
+
if (projectId && accountId && !resolvedAcct.overrodeBinding) {
|
|
430
|
+
// Sticky binding, project half: project-scoped reads (status/logs/routes/
|
|
431
|
+
// drift) resolve their project id from here when no flag is passed.
|
|
432
|
+
await writeProjectBinding(root, accountId, projectId).catch(() => { });
|
|
433
|
+
}
|
|
415
434
|
if (!json && willChangeAnything && !flagYes && isInteractive()) {
|
|
416
435
|
const summary = buildDeployPlanSummary({
|
|
417
436
|
accountId,
|
|
@@ -437,13 +456,31 @@ export async function deployCommand({ argv, root, json, }) {
|
|
|
437
456
|
return cancelledResult(argv, 'deploy');
|
|
438
457
|
}
|
|
439
458
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
459
|
+
// The schema apply is the first phase that touches the portal. A failure here
|
|
460
|
+
// used to throw a raw HubSpot error out of the command — no HSX_E code, no
|
|
461
|
+
// phase/PLAN output (cold-stranger run #3, finding A). Render the curated
|
|
462
|
+
// failure WITH the plan that was being applied instead.
|
|
463
|
+
let portalSchemaApply;
|
|
464
|
+
if (applySchema) {
|
|
465
|
+
try {
|
|
466
|
+
portalSchemaApply = await applyPortalSchemaPlan({
|
|
467
|
+
argv,
|
|
468
|
+
plan: portalSchemaPlan,
|
|
469
|
+
source: portalSchemaRead?.source,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
return portalSchemaApplyFailure({
|
|
474
|
+
argv,
|
|
475
|
+
json,
|
|
476
|
+
root,
|
|
477
|
+
startedAt: commandStartedAt,
|
|
478
|
+
workers,
|
|
479
|
+
portalSchemaPlan,
|
|
480
|
+
error,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
447
484
|
const manifest = await generateProjectArtifacts({
|
|
448
485
|
root,
|
|
449
486
|
workers,
|
|
@@ -906,6 +943,11 @@ function renderDeployHuman(input) {
|
|
|
906
943
|
.map((worker) => `${worker.workerName}${worker.dryRun ? ' (dry-run)' : ''}`)
|
|
907
944
|
.join(', ');
|
|
908
945
|
reporter.info(`Cloudflare deploy: ${deployed}`);
|
|
946
|
+
for (const worker of input.cloudflareDeployResult.workers) {
|
|
947
|
+
if (worker.unauthenticatedInvoke) {
|
|
948
|
+
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).`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
909
951
|
}
|
|
910
952
|
if (!input.linked && input.cloudflareDeployResult) {
|
|
911
953
|
reporter.info(`Deploy history for this machine: ${machineDashboardUrl(input.machineId)}`);
|
|
@@ -927,6 +969,22 @@ function renderDeployHuman(input) {
|
|
|
927
969
|
if (upload.deployId !== undefined) {
|
|
928
970
|
reporter.info(`HubSpot deploy id: ${upload.deployId}`);
|
|
929
971
|
}
|
|
972
|
+
else if (hubspotBuildStatus === 'SUCCESS' &&
|
|
973
|
+
upload.deploySkipped &&
|
|
974
|
+
upload.deployStatus !== undefined) {
|
|
975
|
+
const autoStatus = readStatus(upload.deployStatus) ?? 'UNKNOWN';
|
|
976
|
+
if (autoStatus === 'SUCCESS') {
|
|
977
|
+
reporter.info(`HubSpot deploy: auto-deploy SUCCESS (build #${upload.buildId})`);
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
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}).`);
|
|
981
|
+
for (const subbuild of readSubbuildStatuses(upload.deployStatus)) {
|
|
982
|
+
if (subbuild.status !== 'SUCCESS') {
|
|
983
|
+
reporter.info(` [x] ${subbuild.name}: ${subbuild.errorMessage ?? subbuild.status}`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
930
988
|
else if (hubspotBuildStatus === 'SUCCESS' && upload.deploySkipped) {
|
|
931
989
|
reporter.info('HubSpot deploy: skipped (upload-only or auto-deploy handles it)');
|
|
932
990
|
}
|
|
@@ -1077,13 +1135,24 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
|
|
|
1077
1135
|
});
|
|
1078
1136
|
const applied = [];
|
|
1079
1137
|
const skipped = [];
|
|
1138
|
+
// Each HubSpot write is wrapped so a failure carries WHICH action failed, the
|
|
1139
|
+
// target object type (scope errors are per-object), and what already applied
|
|
1140
|
+
// before it — the curated failure renderer needs all three.
|
|
1141
|
+
const apply = async (label, objectType, run) => {
|
|
1142
|
+
try {
|
|
1143
|
+
await run();
|
|
1144
|
+
}
|
|
1145
|
+
catch (cause) {
|
|
1146
|
+
throw new PortalSchemaApplyError({ action: label, objectType, applied: [...applied], cause });
|
|
1147
|
+
}
|
|
1148
|
+
applied.push(label);
|
|
1149
|
+
};
|
|
1080
1150
|
for (const action of plan.actions) {
|
|
1081
1151
|
if (action.kind === 'will-create-object') {
|
|
1082
|
-
await client.schemaManagement.createSchema({
|
|
1152
|
+
await apply(`create-object:${action.objectType}`, action.objectType, () => client.schemaManagement.createSchema({
|
|
1083
1153
|
name: action.objectType,
|
|
1084
1154
|
labels: portalObjectLabels(action.objectType),
|
|
1085
|
-
});
|
|
1086
|
-
applied.push(`create-object:${action.objectType}`);
|
|
1155
|
+
}));
|
|
1087
1156
|
continue;
|
|
1088
1157
|
}
|
|
1089
1158
|
if (!action.propertyName || !action.declaredType) {
|
|
@@ -1092,25 +1161,23 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
|
|
|
1092
1161
|
}
|
|
1093
1162
|
if (action.kind === 'will-create-property') {
|
|
1094
1163
|
const fieldType = defaultHubSpotFieldType(action.declaredType);
|
|
1095
|
-
await client.schemaManagement.createProperty({
|
|
1164
|
+
await apply(`create-property:${action.objectType}.${action.propertyName}`, action.objectType, () => client.schemaManagement.createProperty({
|
|
1096
1165
|
objectType: action.objectType,
|
|
1097
1166
|
name: action.propertyName,
|
|
1098
1167
|
label: propertyLabel(action.propertyName),
|
|
1099
1168
|
type: action.declaredType,
|
|
1100
1169
|
...(fieldType ? { fieldType } : {}),
|
|
1101
|
-
});
|
|
1102
|
-
applied.push(`create-property:${action.objectType}.${action.propertyName}`);
|
|
1170
|
+
}));
|
|
1103
1171
|
continue;
|
|
1104
1172
|
}
|
|
1105
1173
|
if (action.kind === 'will-alter-property') {
|
|
1106
1174
|
const fieldType = defaultHubSpotFieldType(action.declaredType);
|
|
1107
|
-
await client.schemaManagement.updateProperty({
|
|
1175
|
+
await apply(`alter-property:${action.objectType}.${action.propertyName}`, action.objectType, () => client.schemaManagement.updateProperty({
|
|
1108
1176
|
objectType: action.objectType,
|
|
1109
1177
|
propertyName: action.propertyName,
|
|
1110
1178
|
type: action.declaredType,
|
|
1111
1179
|
...(fieldType ? { fieldType } : {}),
|
|
1112
|
-
});
|
|
1113
|
-
applied.push(`alter-property:${action.objectType}.${action.propertyName}`);
|
|
1180
|
+
}));
|
|
1114
1181
|
}
|
|
1115
1182
|
}
|
|
1116
1183
|
return {
|
|
@@ -1119,6 +1186,198 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
|
|
|
1119
1186
|
skipped,
|
|
1120
1187
|
};
|
|
1121
1188
|
}
|
|
1189
|
+
/** A portal-schema apply action failed against HubSpot. */
|
|
1190
|
+
class PortalSchemaApplyError extends Error {
|
|
1191
|
+
action;
|
|
1192
|
+
objectType;
|
|
1193
|
+
/** Actions that DID apply before the failure (partial-apply visibility). */
|
|
1194
|
+
applied;
|
|
1195
|
+
cause;
|
|
1196
|
+
constructor(input) {
|
|
1197
|
+
super(input.cause instanceof Error ? input.cause.message : String(input.cause));
|
|
1198
|
+
this.name = 'PortalSchemaApplyError';
|
|
1199
|
+
this.action = input.action;
|
|
1200
|
+
this.objectType = input.objectType;
|
|
1201
|
+
this.applied = input.applied;
|
|
1202
|
+
this.cause = input.cause;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* The raw HubSpot response body off a failed schema call. local-dev-lib wraps
|
|
1207
|
+
* axios failures in `HubSpotHttpError` and lifts `response.data` to `.data`;
|
|
1208
|
+
* a raw axios error keeps it at `.cause.response.data`.
|
|
1209
|
+
*/
|
|
1210
|
+
function hubSpotErrorBody(error) {
|
|
1211
|
+
if (!isRecord(error))
|
|
1212
|
+
return undefined;
|
|
1213
|
+
if (error.data !== undefined)
|
|
1214
|
+
return error.data;
|
|
1215
|
+
const cause = error.cause;
|
|
1216
|
+
if (isRecord(cause) && isRecord(cause.response))
|
|
1217
|
+
return cause.response.data;
|
|
1218
|
+
return undefined;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Extract HubSpot's MISSING_SCOPES payload. Captured live (cold-stranger run #3
|
|
1222
|
+
* repro — POST crm/v3/properties/contacts with a PAK lacking the write scope):
|
|
1223
|
+
*
|
|
1224
|
+
* { "status": "error",
|
|
1225
|
+
* "message": "This app hasn't been granted all required scopes to make this call. …",
|
|
1226
|
+
* "category": "MISSING_SCOPES",
|
|
1227
|
+
* "correlationId": "…",
|
|
1228
|
+
* "errors": [{ "message": "One or more of the following scopes are required.",
|
|
1229
|
+
* "context": { "requiredGranularScopes": ["crm.schemas.contacts.write", …] } }] }
|
|
1230
|
+
*
|
|
1231
|
+
* The list is ANY-OF (any one scope grants the call). Older error shapes use
|
|
1232
|
+
* `requiredScopes` (the only key local-dev-lib knows — which is why its own
|
|
1233
|
+
* message renders an EMPTY scope list for granular-scope errors).
|
|
1234
|
+
* Returns undefined when the error is not a scope error.
|
|
1235
|
+
*/
|
|
1236
|
+
function hubSpotMissingScopes(error) {
|
|
1237
|
+
const body = hubSpotErrorBody(error);
|
|
1238
|
+
if (!isRecord(body))
|
|
1239
|
+
return undefined;
|
|
1240
|
+
const scopes = [];
|
|
1241
|
+
if (Array.isArray(body.errors)) {
|
|
1242
|
+
for (const entry of body.errors) {
|
|
1243
|
+
if (!isRecord(entry) || !isRecord(entry.context))
|
|
1244
|
+
continue;
|
|
1245
|
+
for (const key of ['requiredGranularScopes', 'requiredScopes']) {
|
|
1246
|
+
const list = entry.context[key];
|
|
1247
|
+
if (!Array.isArray(list))
|
|
1248
|
+
continue;
|
|
1249
|
+
for (const scope of list) {
|
|
1250
|
+
if (typeof scope === 'string')
|
|
1251
|
+
scopes.push(scope);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (scopes.length === 0 && body.category !== 'MISSING_SCOPES')
|
|
1257
|
+
return undefined;
|
|
1258
|
+
return {
|
|
1259
|
+
scopes: [...new Set(scopes)],
|
|
1260
|
+
...(typeof body.correlationId === 'string' ? { correlationId: body.correlationId } : {}),
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
// Object types with a dedicated property-settings write scope
|
|
1264
|
+
// (crm.schemas.<type>.write — "create, delete, or make changes to property
|
|
1265
|
+
// settings"; crm.objects.<type>.write is RECORD writes and does not grant
|
|
1266
|
+
// property creation). Everything else falls under crm.schemas.custom.write.
|
|
1267
|
+
const HUBSPOT_SCHEMA_SCOPE_OBJECTS = new Set([
|
|
1268
|
+
'appointments',
|
|
1269
|
+
'carts',
|
|
1270
|
+
'commercepayments',
|
|
1271
|
+
'companies',
|
|
1272
|
+
'contacts',
|
|
1273
|
+
'courses',
|
|
1274
|
+
'deals',
|
|
1275
|
+
'invoices',
|
|
1276
|
+
'line_items',
|
|
1277
|
+
'listings',
|
|
1278
|
+
'orders',
|
|
1279
|
+
'quotes',
|
|
1280
|
+
'services',
|
|
1281
|
+
'subscriptions',
|
|
1282
|
+
'tickets',
|
|
1283
|
+
'users',
|
|
1284
|
+
]);
|
|
1285
|
+
/** The property-definition write scope `--apply-schema` needs for an object type. */
|
|
1286
|
+
function propertyWriteScopeFor(objectType) {
|
|
1287
|
+
return HUBSPOT_SCHEMA_SCOPE_OBJECTS.has(objectType)
|
|
1288
|
+
? `crm.schemas.${objectType}.write`
|
|
1289
|
+
: 'crm.schemas.custom.write';
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Narrow HubSpot's any-of scope list to what actually unblocks THIS apply:
|
|
1293
|
+
* the schemas-write scope for the target object when present, else every scope
|
|
1294
|
+
* that names the object, else the full response list (else the doc-grounded
|
|
1295
|
+
* default for the object type when the response carried none).
|
|
1296
|
+
*/
|
|
1297
|
+
function relevantMissingScopes(all, objectType) {
|
|
1298
|
+
const expected = propertyWriteScopeFor(objectType);
|
|
1299
|
+
if (all.includes(expected))
|
|
1300
|
+
return [expected];
|
|
1301
|
+
const mentioning = all.filter((scope) => scope.includes(`.${objectType}.`));
|
|
1302
|
+
if (mentioning.length > 0)
|
|
1303
|
+
return mentioning;
|
|
1304
|
+
return all.length > 0 ? all : [expected];
|
|
1305
|
+
}
|
|
1306
|
+
const HUBSPOT_PAK_URL = 'https://app.hubspot.com/l/personal-access-key';
|
|
1307
|
+
/**
|
|
1308
|
+
* Curated rendering for a failed `--apply-schema` leg (cold-stranger run #3,
|
|
1309
|
+
* finding A): the phase + PLAN output still prints, partial progress is shown,
|
|
1310
|
+
* and a HubSpot MISSING_SCOPES response becomes HSX_E_DEPLOY_SCHEMA_SCOPES
|
|
1311
|
+
* naming the exact scopes to grant — never a raw truncated HubSpot error.
|
|
1312
|
+
*/
|
|
1313
|
+
function portalSchemaApplyFailure(input) {
|
|
1314
|
+
const applyError = input.error instanceof PortalSchemaApplyError ? input.error : undefined;
|
|
1315
|
+
const cause = applyError?.cause ?? input.error;
|
|
1316
|
+
const scopeError = hubSpotMissingScopes(cause);
|
|
1317
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
1318
|
+
const hubSpotDetail = `${causeMessage.split('\n')[0]}${scopeError?.correlationId ? ` (correlationId ${scopeError.correlationId})` : ''}`;
|
|
1319
|
+
let code;
|
|
1320
|
+
let message;
|
|
1321
|
+
let hint;
|
|
1322
|
+
let missingScopes = [];
|
|
1323
|
+
if (scopeError && applyError) {
|
|
1324
|
+
code = 'HSX_E_DEPLOY_SCHEMA_SCOPES';
|
|
1325
|
+
missingScopes = relevantMissingScopes(scopeError.scopes, applyError.objectType);
|
|
1326
|
+
const scopesText = missingScopes.length === 1
|
|
1327
|
+
? `the ${missingScopes[0]} scope`
|
|
1328
|
+
: `one of these scopes: ${missingScopes.join(', ')}`;
|
|
1329
|
+
message = `HubSpot rejected ${applyError.action}: your personal access key cannot write ${applyError.objectType} property definitions. Grant ${scopesText}.`;
|
|
1330
|
+
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.`;
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
code = 'HSX_E_DEPLOY_SCHEMA_APPLY';
|
|
1334
|
+
message = applyError
|
|
1335
|
+
? `Portal schema apply failed at ${applyError.action}: ${causeMessage.split('\n')[0]}`
|
|
1336
|
+
: causeMessage;
|
|
1337
|
+
hint = 'Preview the schema plan without applying: hs-x deploy --plan --portal-schema-live.';
|
|
1338
|
+
}
|
|
1339
|
+
const exitCode = code === 'HSX_E_DEPLOY_SCHEMA_SCOPES' ? 10 : 1;
|
|
1340
|
+
if (input.json) {
|
|
1341
|
+
write(`${JSON.stringify({
|
|
1342
|
+
schema_version: 1,
|
|
1343
|
+
command: 'deploy',
|
|
1344
|
+
ok: false,
|
|
1345
|
+
...(input.portalSchemaPlan ? { portalSchemaPlan: input.portalSchemaPlan } : {}),
|
|
1346
|
+
...(applyError && applyError.applied.length > 0
|
|
1347
|
+
? { portalSchemaApplied: applyError.applied }
|
|
1348
|
+
: {}),
|
|
1349
|
+
error: {
|
|
1350
|
+
code,
|
|
1351
|
+
message,
|
|
1352
|
+
hint,
|
|
1353
|
+
...(missingScopes.length > 0 ? { missing_scopes: missingScopes } : {}),
|
|
1354
|
+
detail: hubSpotDetail,
|
|
1355
|
+
},
|
|
1356
|
+
}, null, 2)}\n`);
|
|
1357
|
+
return { exitCode };
|
|
1358
|
+
}
|
|
1359
|
+
const reporter = createReporter({ command: 'deploy', argv: input.argv, startedAt: input.startedAt });
|
|
1360
|
+
reporter.header(basename(input.root));
|
|
1361
|
+
const capCount = input.workers.reduce((count, worker) => count + worker.capabilities.length, 0);
|
|
1362
|
+
reporter
|
|
1363
|
+
.step('Validating project')
|
|
1364
|
+
.ok(`${input.workers.length} workers, ${capCount} capabilities`);
|
|
1365
|
+
if (input.portalSchemaPlan) {
|
|
1366
|
+
reporter.info(renderPortalSchemaPlan(input.portalSchemaPlan).trimEnd());
|
|
1367
|
+
}
|
|
1368
|
+
if (applyError && applyError.applied.length > 0) {
|
|
1369
|
+
for (const done of applyError.applied)
|
|
1370
|
+
reporter.info(`APPLIED ${done}`);
|
|
1371
|
+
}
|
|
1372
|
+
reporter.step('Applying portal schema').fail(applyError?.action);
|
|
1373
|
+
reporter.error(code, message, {
|
|
1374
|
+
hint,
|
|
1375
|
+
cause: hubSpotDetail,
|
|
1376
|
+
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
1377
|
+
});
|
|
1378
|
+
reporter.done(undefined, exitCode);
|
|
1379
|
+
return { exitCode };
|
|
1380
|
+
}
|
|
1122
1381
|
function renderPortalSchemaPlan(plan) {
|
|
1123
1382
|
if (plan.items.length === 0) {
|
|
1124
1383
|
return 'Portal schema: no changes or warnings.\n';
|
|
@@ -1520,6 +1779,10 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, source
|
|
|
1520
1779
|
process.stderr.write(`warning: could not register the tenant Worker URL with the control plane: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1521
1780
|
}
|
|
1522
1781
|
}
|
|
1782
|
+
// Without the client secret on the Worker, the runtime accepts UNSIGNED
|
|
1783
|
+
// workflow-action invokes (HubSpot must still be able to call them pre-OAuth)
|
|
1784
|
+
// — surface that as a one-line warning instead of leaving it silent.
|
|
1785
|
+
const hasWorkflowActions = worker.capabilities.some((capability) => capability.kind === 'tool');
|
|
1523
1786
|
return {
|
|
1524
1787
|
workerName,
|
|
1525
1788
|
workerSourcePath: relative(root, workerSourcePath),
|
|
@@ -1530,6 +1793,9 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, source
|
|
|
1530
1793
|
dryRun,
|
|
1531
1794
|
stdout: output.stdout,
|
|
1532
1795
|
stderr: output.stderr,
|
|
1796
|
+
...(hasWorkflowActions && !resolvedHubSpotClientSecret && !dryRun
|
|
1797
|
+
? { unauthenticatedInvoke: true }
|
|
1798
|
+
: {}),
|
|
1533
1799
|
};
|
|
1534
1800
|
}
|
|
1535
1801
|
async function discoverWorkerSourcePaths(root) {
|
|
@@ -2351,6 +2617,14 @@ async function executeHubSpotOnlyUpload({ argv, root, local, uploadOnly, }) {
|
|
|
2351
2617
|
: 0;
|
|
2352
2618
|
if (appId)
|
|
2353
2619
|
await persistHubSpotAppBinding(root, { appId, projectName, developerAccountId });
|
|
2620
|
+
// Build SUCCESS does NOT mean the app shipped: HubSpot's auto-deploy can
|
|
2621
|
+
// still fail (found live: "Apps are not allowed to have 0 redirect URLs"
|
|
2622
|
+
// failed deploy #4 while the CLI reported green). Verify the deploy too.
|
|
2623
|
+
// Auto-deploy fires on uploads too — verify it regardless of uploadOnly
|
|
2624
|
+
// (the f1-data 0-redirect failure rode exactly this path).
|
|
2625
|
+
const autoDeployStatus = autoDeployEnabled && readStatus(buildStatus) === 'SUCCESS'
|
|
2626
|
+
? await waitForHubSpotAutoDeploy({ client, projectName, buildStatus })
|
|
2627
|
+
: undefined;
|
|
2354
2628
|
return {
|
|
2355
2629
|
projectName,
|
|
2356
2630
|
archivePath,
|
|
@@ -2360,7 +2634,7 @@ async function executeHubSpotOnlyUpload({ argv, root, local, uploadOnly, }) {
|
|
|
2360
2634
|
buildId: upload.buildId,
|
|
2361
2635
|
deployId: undefined,
|
|
2362
2636
|
buildStatus,
|
|
2363
|
-
deployStatus:
|
|
2637
|
+
deployStatus: autoDeployStatus,
|
|
2364
2638
|
deploySkipped: Boolean(uploadOnly || autoDeployEnabled),
|
|
2365
2639
|
local,
|
|
2366
2640
|
};
|
|
@@ -2401,6 +2675,40 @@ async function optionalHubSpotAppId(client, projectName, appUid) {
|
|
|
2401
2675
|
return 0;
|
|
2402
2676
|
}
|
|
2403
2677
|
}
|
|
2678
|
+
/**
|
|
2679
|
+
* Poll the auto-deploy triggered by a successful build until it reaches a
|
|
2680
|
+
* terminal state. The build status carries the deploy task locator
|
|
2681
|
+
* (deployStatusTaskLocator.id); absent/transient lookups resolve undefined —
|
|
2682
|
+
* the caller treats that as "could not verify", never as success.
|
|
2683
|
+
*/
|
|
2684
|
+
async function waitForHubSpotAutoDeploy({ client, projectName, buildStatus, timeoutMs = 60_000, intervalMs = 2_000, }) {
|
|
2685
|
+
const locator = buildStatus && typeof buildStatus === 'object'
|
|
2686
|
+
? buildStatus.deployStatusTaskLocator
|
|
2687
|
+
: undefined;
|
|
2688
|
+
const deployId = locator && typeof locator.id === 'number'
|
|
2689
|
+
? locator.id
|
|
2690
|
+
: locator && typeof locator.id === 'string'
|
|
2691
|
+
? Number.parseInt(locator.id, 10)
|
|
2692
|
+
: undefined;
|
|
2693
|
+
if (deployId === undefined || Number.isNaN(deployId))
|
|
2694
|
+
return undefined;
|
|
2695
|
+
const deadline = Date.now() + timeoutMs;
|
|
2696
|
+
let last;
|
|
2697
|
+
while (Date.now() < deadline) {
|
|
2698
|
+
try {
|
|
2699
|
+
last = await client.projects.getDeployStatus({ projectName, deployId });
|
|
2700
|
+
}
|
|
2701
|
+
catch {
|
|
2702
|
+
last = undefined;
|
|
2703
|
+
}
|
|
2704
|
+
const status = readStatus(last);
|
|
2705
|
+
if (status && status !== 'PENDING' && status !== 'BUILDING' && status !== 'DEPLOYING') {
|
|
2706
|
+
return last;
|
|
2707
|
+
}
|
|
2708
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
2709
|
+
}
|
|
2710
|
+
return last;
|
|
2711
|
+
}
|
|
2404
2712
|
async function waitForHubSpotBuildStatus({ client, projectName, buildId, timeoutMs, intervalMs, }) {
|
|
2405
2713
|
const startedAt = Date.now();
|
|
2406
2714
|
let latest;
|