@siloy/hubspot-provisioning 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -7,7 +7,7 @@ Requires Node 20 or newer.
7
7
  ## Install
8
8
 
9
9
  ```sh
10
- npx -y @siloy/hubspot-provisioning@0.1.1 validate
10
+ npx -y @siloy/hubspot-provisioning@0.1.2 validate
11
11
  ```
12
12
 
13
13
  In CI, run this pinned `npx` form (see "GitHub Actions"); don't install the CLI as a repo dependency in a job that has the deploy key. For editor support of the schema, a local install is fine: `npm install --save-dev @siloy/hubspot-provisioning`.
@@ -44,7 +44,7 @@ hubspot-provisioning deploy [file] [plan options except --mode]
44
44
  On a `pull_request` event, `GITHUB_SHA` and `GITHUB_REF` are the merge commit and `refs/pull/N/merge`, not your branch. That is what the plan is recorded under unless you pass the head yourself:
45
45
 
46
46
  ```yaml
47
- - run: npx -y @siloy/hubspot-provisioning@0.1.1 plan --mode preview --sha "$HEAD_SHA" --ref "$HEAD_REF"
47
+ - run: npx -y @siloy/hubspot-provisioning@0.1.2 plan --mode preview --sha "$HEAD_SHA" --ref "$HEAD_REF"
48
48
  env:
49
49
  npm_config_registry: https://registry.npmjs.org
50
50
  HEAD_SHA: ${{ github.event.pull_request.head.sha }}
@@ -72,7 +72,9 @@ Pass them through `env:`, never inline in the `run:` text: a branch name is atta
72
72
 
73
73
  ## Getting a deploy key
74
74
 
75
- A deploy key belongs to a workspace and is bound to one portal and blueprint. A workspace owner creates it on the blueprint page in the provisioning app and either:
75
+ A deploy key belongs to a workspace and is bound to one portal and blueprint. It is either "Preview only" (it can run `plan --mode preview` and nothing else) or "Preview and deploy". Use a preview-only key in the pull-request job and the deploy key only in the job that runs on `main`: two secrets, `PROVISIONING_PREVIEW_KEY` and `PROVISIONING_KEY` in the samples below. A preview-only key gets `preview_only_key` (exit 1) for a deploy plan or an apply. Its previews compare with what the deploy key last deployed to the portal, so they flag the same hand edits in HubSpot a deploy would. This needs exactly one deploy key (not revoked) for the blueprint and portal; with several, previews block every update.
76
+
77
+ A workspace owner creates a key on the blueprint page in the provisioning app, chooses its access, and either:
76
78
 
77
79
  - copies the secret and puts it in the repo's CI secrets, or
78
80
  - chooses "Create a link for a developer" and sends you the link. The link works once and expires. Open it, click "Reveal secret" and copy the `hspk_` secret into the repo's CI secrets. If the link expired unused, ask the owner for a new one. If it was already opened (by you or someone else), the owner revokes the key and creates a new one.
@@ -83,7 +85,7 @@ A key expires after 90 days without use, counted from its last use, creation or
83
85
 
84
86
  ## GitHub Actions
85
87
 
86
- Preview on pull requests (with a comment that is updated, not repeated) and deploy on pushes to `main`. Pin the actions by commit SHA as your own policy requires; versions are shown here for readability. The samples pin 0.1.1; pin the version you use and bump it on purpose.
88
+ Preview on pull requests (with a comment that is updated, not repeated) and deploy on pushes to `main`. Pin the actions by commit SHA as your own policy requires; versions are shown here for readability. The samples pin 0.1.2; pin the version you use and bump it on purpose.
87
89
 
88
90
  ```yaml
89
91
  # .github/workflows/blueprint.yml
@@ -109,13 +111,13 @@ jobs:
109
111
  - uses: actions/setup-node@v7
110
112
  with:
111
113
  node-version: 24
112
- # No npm ci: the job holds the deploy key, so it runs only the pinned CLI, never the branch's dependencies.
114
+ # No npm ci: the job holds a key (the preview-only one, which can still plan and read the portal), so it runs only the pinned CLI, never the branch's dependencies.
113
115
  # npm_config_registry outranks a project .npmrc, so a branch cannot point npx at another registry.
114
- - run: npx -y @siloy/hubspot-provisioning@0.1.1 validate
116
+ - run: npx -y @siloy/hubspot-provisioning@0.1.2 validate
115
117
  env:
116
118
  npm_config_registry: https://registry.npmjs.org
117
119
  - run: >-
118
- npx -y @siloy/hubspot-provisioning@0.1.1 plan --mode preview
120
+ npx -y @siloy/hubspot-provisioning@0.1.2 plan --mode preview
119
121
  --comment-file plan-comment.md
120
122
  --sha "$HEAD_SHA" --ref "$HEAD_REF"
121
123
  env:
@@ -123,7 +125,8 @@ jobs:
123
125
  HEAD_REF: ${{ github.head_ref }}
124
126
  npm_config_registry: https://registry.npmjs.org
125
127
  PROVISIONING_URL: ${{ vars.PROVISIONING_URL }}
126
- PROVISIONING_KEY: ${{ secrets.PROVISIONING_KEY }}
128
+ # The preview-only key: it cannot deploy.
129
+ PROVISIONING_KEY: ${{ secrets.PROVISIONING_PREVIEW_KEY }}
127
130
  # Runs also when the plan failed or was refused: the comment says why.
128
131
  - name: Post the plan on the pull request
129
132
  if: always() && hashFiles('plan-comment.md') != ''
@@ -153,7 +156,7 @@ jobs:
153
156
  - uses: actions/setup-node@v7
154
157
  with:
155
158
  node-version: 24
156
- - run: npx -y @siloy/hubspot-provisioning@0.1.1 deploy
159
+ - run: npx -y @siloy/hubspot-provisioning@0.1.2 deploy
157
160
  env:
158
161
  npm_config_registry: https://registry.npmjs.org
159
162
  PROVISIONING_URL: ${{ vars.PROVISIONING_URL }}
@@ -171,7 +174,10 @@ A deploy that would overwrite something someone changed by hand in HubSpot is re
171
174
 
172
175
  ### Trust boundary
173
176
 
174
- `PROVISIONING_KEY` is like any GitHub secret: anyone who can push a branch to the integration repo can change the workflow on that branch and use the key. Keep write access to the integration repo to trusted developers. A key can deploy as well as preview, so a preview job's key could also deploy to its portal; a preview-only key is planned.
177
+ A key in a GitHub secret is like any GitHub secret: anyone who can push a branch to the integration repo can change the workflow on that branch and use it. Give the pull-request job a preview-only key, so the secret that job needs cannot deploy. That alone does not keep the deploy key in `main`'s job:
178
+
179
+ - Where your GitHub plan has environments (public repos, or GitHub Pro, Team or Enterprise for private repos), store `PROVISIONING_KEY` as a secret of an environment whose deployment branches are limited to `main`, and give the deploy job `environment:` with that name. A workflow on another branch then cannot read it.
180
+ - Without environments (GitHub Free with a private repo), every repository secret is readable by a workflow on any branch, the deploy key included. Keep write access to the integration repo to trusted developers.
175
181
 
176
182
  ## Editor support
177
183
 
@@ -195,7 +201,7 @@ The schema is also available as `@siloy/hubspot-provisioning/schema.json`. It de
195
201
 
196
202
  ## Upgrading
197
203
 
198
- When the blueprint rules change, the server may refuse older CLIs with `cli_outdated`. Upgrade with `npm install --save-dev @siloy/hubspot-provisioning@latest`, or raise the version in your workflow's `npx -y @siloy/hubspot-provisioning@0.1.1`.
204
+ When the blueprint rules change, the server may refuse older CLIs with `cli_outdated`. Upgrade with `npm install --save-dev @siloy/hubspot-provisioning@latest`, or raise the version in your workflow's `npx -y @siloy/hubspot-provisioning@0.1.2`.
199
205
 
200
206
  When a command fails after the server created a run, it prints `Plan run:` and `Apply run:` ids; look them up in the provisioning app. After a timeout or error while waiting for an apply, the apply may still be running: check it before re-running.
201
207
 
@@ -6,7 +6,7 @@
6
6
  * An error, refusal or run error code. The listed ones are those docs/ci-api.md documents, plus the client's own
7
7
  * `network`, `http_error` and `unexpected_response`; the server may add codes, so any string is accepted.
8
8
  */
9
- export type KnownErrorCode = 'invalid' | 'cli_outdated' | 'unauthenticated' | 'forbidden' | 'deploy_key_expired' | 'not_found' | 'preview_plan' | 'plan_unclassified' | 'blocking_updates' | 'mapping_target_missing' | 'nothing_to_apply' | 'reconnect_required' | 'apply_locked' | 'missing_scopes' | 'conflict' | 'too_large' | 'rate_limited' | 'internal' | 'environment' | 'hubspot' | 'plan_changed' | 'apply_failed' | 'deploy_key_revoked' | 'portal_moved' | 'dispatch_failed' | 'timed_out' | 'stopped' | 'never_started' | 'input' | 'network' | 'http_error' | 'unexpected_response' | (string & {});
9
+ export type KnownErrorCode = 'invalid' | 'cli_outdated' | 'unauthenticated' | 'forbidden' | 'deploy_key_expired' | 'preview_only_key' | 'not_found' | 'preview_plan' | 'plan_unclassified' | 'blocking_updates' | 'mapping_target_missing' | 'nothing_to_apply' | 'reconnect_required' | 'apply_locked' | 'missing_scopes' | 'conflict' | 'too_large' | 'rate_limited' | 'internal' | 'environment' | 'hubspot' | 'plan_changed' | 'apply_failed' | 'deploy_key_revoked' | 'portal_moved' | 'dispatch_failed' | 'timed_out' | 'stopped' | 'never_started' | 'input' | 'network' | 'http_error' | 'unexpected_response' | (string & {});
10
10
  export type CiMode = 'preview' | 'deploy';
11
11
  export type RunStatus = 'queued' | 'running' | 'succeeded' | 'failed';
12
12
  export interface CreatePlanRequest {
@@ -65,6 +65,7 @@ export interface CiRunView {
65
65
  blueprint: unknown;
66
66
  portal: unknown;
67
67
  remediation: string;
68
+ option?: 'allowRetype' | 'pruneOptions';
68
69
  }[];
69
70
  warnings: string[];
70
71
  blocking: BlockingUpdate[];
@@ -75,11 +76,15 @@ export interface CiRunView {
75
76
  property: string | null;
76
77
  }[];
77
78
  baselineInvalid: boolean;
79
+ /** A preview-only key's blueprint and portal have no live deploy key, or several; absent from older servers. */
80
+ deployKeyGap?: 'no_deploy_key' | 'several_deploy_keys' | null;
78
81
  appliable: boolean;
79
82
  refusal: {
80
83
  code: KnownErrorCode;
81
84
  message: string;
82
85
  } | null;
86
+ /** How to finish conflicts only allowRetype or pruneOptions resolves, which CI never sets; absent from older servers. */
87
+ finish?: string | null;
83
88
  applyRunId: string | null;
84
89
  error: {
85
90
  code: KnownErrorCode;
@@ -4,6 +4,7 @@ const HINTS = {
4
4
  deploy_key_expired: 'Ask a workspace owner to reactivate the key on the blueprint page, then re-run this job. The secret stays the same.',
5
5
  unauthenticated: 'The key is not valid (unknown, revoked or mistyped). Ask a workspace owner for a new key or a one-time link, and update the PROVISIONING_KEY secret.',
6
6
  forbidden: "The key's portal is no longer in the workspace.",
7
+ preview_only_key: 'Check which secret this job uses: the pull-request job gets the preview-only key, the job on the main branch a key that can deploy.',
7
8
  deploy_key_revoked: 'The key was revoked; an apply may already have changed the portal. Plan again with a new key.',
8
9
  reconnect_required: RECONNECT,
9
10
  missing_scopes: RECONNECT,
@@ -3,13 +3,17 @@ import { hintFor } from './hints.js';
3
3
  export const COMMENT_MARKER = '<!-- hubspot-provisioning -->';
4
4
  const MAX_COMMENT_ISSUES = 10;
5
5
  const BASELINE_INVALID = "This key's last deploy no longer passes today's blueprint rules, so every update blocks until a deploy with nothing to apply.";
6
+ const DEPLOY_KEY_GAP = {
7
+ no_deploy_key: 'This preview-only key compares with what the deploy key last deployed, but this blueprint and portal have no deploy key, so every update blocks. Create one for the job on the main branch.',
8
+ several_deploy_keys: 'This preview-only key compares with what the deploy key last deployed, but this blueprint and portal have several, and which one deploys is unknown, so every update blocks. Revoke the ones no job uses.',
9
+ };
6
10
  const ACTIONS = {
7
11
  plan_again: 'plan again: re-run this job',
8
12
  reconnect: 'a person reconnects the portal in the provisioning app',
9
13
  };
10
14
  const REASONS = {
11
15
  changed_in_hubspot: 'changed in HubSpot since the last deploy',
12
- no_baseline: 'no earlier deploy by this key to compare with',
16
+ no_baseline: 'no earlier deploy to compare with',
13
17
  };
14
18
  /** The console lines for a finished run (#105). Server text is printed as is. */
15
19
  export function renderRunText(run) {
@@ -18,11 +22,15 @@ export function renderRunText(run) {
18
22
  lines.push(`Changes: ${counts(run.summary) || 'none'}`);
19
23
  section(lines, 'Actions:', run.actions.map((a) => `${a.type} ${a.target}: ${a.summary}`));
20
24
  section(lines, 'Conflicts:', run.conflicts.map((c) => `${c.target} ${c.field}: ${c.remediation}`));
25
+ if (run.finish)
26
+ lines.push(`To finish: ${run.finish}`);
21
27
  section(lines, 'Blocking updates (hand edits in HubSpot the apply would overwrite):', run.blocking.map(blockingText));
22
28
  section(lines, 'Missing mapping targets (in neither the blueprint nor HubSpot):', run.missingTargets.map(targetName));
23
29
  section(lines, 'Warnings:', run.warnings);
24
30
  if (run.baselineInvalid)
25
31
  lines.push(BASELINE_INVALID);
32
+ if (run.deployKeyGap)
33
+ lines.push(DEPLOY_KEY_GAP[run.deployKeyGap]);
26
34
  if (run.refusal) {
27
35
  lines.push(run.refusal.message);
28
36
  pushHint(lines, run.refusal.code);
@@ -47,11 +55,15 @@ export function renderComment(run, context) {
47
55
  parts.push(about, '', run.summary ? `**Summary:** ${md(counts(run.summary)) || 'no changes.'}` : '**Summary:** unknown, the plan did not finish.', '');
48
56
  mdSection(parts, 'Changes', run.actions.map((a) => `**${md(a.type)}** ${md(a.target)}: ${md(a.summary)}`));
49
57
  mdSection(parts, 'Conflicts', run.conflicts.map((c) => `${md(c.target)} ${md(c.field)}: ${md(c.remediation)}`));
58
+ if (run.finish)
59
+ parts.push(`**To finish:** ${md(run.finish)}`, '');
50
60
  mdSection(parts, 'Blocking updates (hand edits in HubSpot)', run.blocking.map((b) => md(blockingText(b))));
51
61
  mdSection(parts, 'Missing mapping targets', run.missingTargets.map((t) => md(targetName(t))));
52
62
  mdSection(parts, 'Warnings', run.warnings.map(md));
53
63
  if (run.baselineInvalid)
54
64
  parts.push(md(BASELINE_INVALID), '');
65
+ if (run.deployKeyGap)
66
+ parts.push(md(DEPLOY_KEY_GAP[run.deployKeyGap]), '');
55
67
  parts.push(verdict(run));
56
68
  return `${parts.join('\n')}\n`;
57
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siloy/hubspot-provisioning",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Validate HubSpot provisioning blueprints offline and deploy them from CI.",
5
5
  "type": "module",
6
6
  "engines": { "node": ">=20" },