@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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
}
|