@liiift-studio/deploy-vercel-from-sanity 0.2.0 → 0.2.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 +44 -49
- package/dist/index.js +30 -32
- package/dist/index.mjs +30 -32
- package/package.json +10 -4
- package/src/components/DeployItem.tsx +12 -12
package/README.md
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
# deploy-vercel-from-sanity
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Trigger and monitor Vercel deployments directly from [Sanity Studio](https://www.sanity.io) — no context switching required.**
|
|
4
4
|
|
|
5
|
-
](https://www.npmjs.com/package/@liiift-studio/deploy-vercel-from-sanity)
|
|
6
|
+
[](https://www.sanity.io)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
---
|
|
6
12
|
|
|
7
13
|
## Features
|
|
8
14
|
|
|
9
|
-
- **
|
|
15
|
+
- **One-click deploy** — trigger Production or Preview builds from inside Sanity Studio
|
|
10
16
|
- **Live status** with automatic polling — Queued → Building → Ready / Error
|
|
11
|
-
- **Build timer** showing elapsed time while a deploy is
|
|
17
|
+
- **Build timer** showing elapsed time while a deploy is in progress
|
|
12
18
|
- **Cancel** in-progress deployments
|
|
13
19
|
- **Copy deployment URL** with one click
|
|
14
|
-
- **Inline error log viewer** — see build errors
|
|
20
|
+
- **Inline error log viewer** — see build errors without leaving the studio
|
|
15
21
|
- **Deployment history** per target
|
|
16
|
-
- **"Open in Vercel"** link to
|
|
22
|
+
- **"Open in Vercel"** link to your project dashboard
|
|
23
|
+
- **Multiple targets** — configure Production, Preview, and any number of custom environments
|
|
24
|
+
- **Shared API token** — set it once; all authenticated studio users with editor access or above can deploy
|
|
17
25
|
- **Responsive grid layout** — 2 columns on desktop, 1 on mobile
|
|
18
|
-
- **Shared API token** — set it once, works for all studio users with editor access or above
|
|
19
26
|
|
|
20
27
|
---
|
|
21
28
|
|
|
@@ -27,7 +34,7 @@ npm install @liiift-studio/deploy-vercel-from-sanity
|
|
|
27
34
|
|
|
28
35
|
---
|
|
29
36
|
|
|
30
|
-
##
|
|
37
|
+
## Quick start
|
|
31
38
|
|
|
32
39
|
### 1. Add the plugin to your Sanity config
|
|
33
40
|
|
|
@@ -39,38 +46,22 @@ import { vercelDeploy } from '@liiift-studio/deploy-vercel-from-sanity'
|
|
|
39
46
|
export default defineConfig({
|
|
40
47
|
// ...
|
|
41
48
|
plugins: [
|
|
42
|
-
vercelDeploy(),
|
|
43
|
-
// or with a custom label / name:
|
|
44
49
|
vercelDeploy({ title: 'Deploy', name: 'vercel-deploy' }),
|
|
45
50
|
],
|
|
46
51
|
})
|
|
47
52
|
```
|
|
48
53
|
|
|
49
|
-
### 2.
|
|
54
|
+
### 2. Connect your Vercel API token
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
// sanity.config.ts
|
|
55
|
-
tools: (prev, { currentUser }) => {
|
|
56
|
-
const canDeploy = currentUser?.roles?.some(r =>
|
|
57
|
-
['administrator', 'editor'].includes(r.name)
|
|
58
|
-
)
|
|
59
|
-
return canDeploy ? prev : prev.filter(t => t.name !== 'vercel-deploy')
|
|
60
|
-
},
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### 3. Connect your Vercel API token
|
|
64
|
-
|
|
65
|
-
Open the **Deploy** tool in Sanity Studio. Enter a Vercel API token when prompted.
|
|
56
|
+
Open the **Deploy** tab in Sanity Studio and enter a Vercel API token when prompted.
|
|
66
57
|
|
|
67
58
|
To create a token: **vercel.com → Settings → Tokens → Create → Full Account scope**.
|
|
68
59
|
|
|
69
|
-
The token is stored in a `config.vercelDeploy` document in your dataset and
|
|
60
|
+
The token is stored in a `config.vercelDeploy` document in your dataset and shared across all authenticated studio users.
|
|
70
61
|
|
|
71
|
-
###
|
|
62
|
+
### 3. Add a deploy target
|
|
72
63
|
|
|
73
|
-
Create one or more `vercel_deploy` documents
|
|
64
|
+
Create one or more `vercel_deploy` documents — each represents an environment (Production, Preview, etc.).
|
|
74
65
|
|
|
75
66
|
**Via Sanity CLI:**
|
|
76
67
|
|
|
@@ -87,11 +78,11 @@ EOF
|
|
|
87
78
|
|
|
88
79
|
**To get your deploy hook URL:** Vercel Dashboard → Project → Settings → Git → Deploy Hooks → Create Hook.
|
|
89
80
|
|
|
90
|
-
**
|
|
81
|
+
**Available fields on each `vercel_deploy` document:**
|
|
91
82
|
|
|
92
83
|
| Field | Type | Required | Description |
|
|
93
84
|
|---|---|---|---|
|
|
94
|
-
| `name` | `string` | ✓ | Display label
|
|
85
|
+
| `name` | `string` | ✓ | Display label (e.g. "Production", "Preview") |
|
|
95
86
|
| `url` | `url` | ✓ | Vercel deploy hook URL |
|
|
96
87
|
| `teamId` | `string` | | Vercel team ID — required for team-owned projects |
|
|
97
88
|
| `disableDeleteAction` | `boolean` | | Hides the delete button for this target in the studio UI |
|
|
@@ -108,6 +99,22 @@ EOF
|
|
|
108
99
|
|
|
109
100
|
---
|
|
110
101
|
|
|
102
|
+
## Restrict access to editors and above
|
|
103
|
+
|
|
104
|
+
By default the Deploy tab is visible to all authenticated users. To hide it from viewers:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// sanity.config.ts
|
|
108
|
+
tools: (prev, { currentUser }) => {
|
|
109
|
+
const canDeploy = currentUser?.roles?.some(r =>
|
|
110
|
+
['administrator', 'editor'].includes(r.name)
|
|
111
|
+
)
|
|
112
|
+
return canDeploy ? prev : prev.filter(t => t.name !== 'vercel-deploy')
|
|
113
|
+
},
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
111
118
|
## How it works
|
|
112
119
|
|
|
113
120
|
1. Deploy targets are stored as `vercel_deploy` documents in your Sanity dataset.
|
|
@@ -120,25 +127,13 @@ EOF
|
|
|
120
127
|
|
|
121
128
|
## Security
|
|
122
129
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
The Vercel API token is stored in a `config.vercelDeploy` Sanity document, which is readable by **all authenticated studio users**. Anyone with a login to your Sanity project can read the token value via the Sanity API.
|
|
126
|
-
|
|
127
|
-
**Recommendations:**
|
|
128
|
-
- Audit who has access to your Sanity project at sanity.io → Project → Members.
|
|
129
|
-
- If your studio includes untrusted editors, consider a server-side proxy that holds the token outside Sanity and exposes only a scoped deploy endpoint.
|
|
130
|
-
|
|
131
|
-
### Deploy hook URL validation
|
|
132
|
-
|
|
133
|
-
`triggerDeploy` validates that the hook URL matches `api.vercel.com/v1/integrations/deploy/` before making the request. This prevents SSRF if a `vercel_deploy` document is created or modified outside the Studio's schema validation.
|
|
134
|
-
|
|
135
|
-
### External links
|
|
130
|
+
**API token storage** — The Vercel API token is stored in a `config.vercelDeploy` Sanity document, readable by all authenticated studio users. Audit who has access to your Sanity project at sanity.io → Project → Members. If your studio includes untrusted editors, consider a server-side proxy that holds the token and exposes only a scoped deploy endpoint.
|
|
136
131
|
|
|
137
|
-
|
|
132
|
+
**Deploy hook URL validation** — `triggerDeploy` validates that the hook URL matches `api.vercel.com/v1/integrations/deploy/` before making the request, preventing SSRF from a tampered document.
|
|
138
133
|
|
|
139
|
-
|
|
134
|
+
**External links** — All external links use `target="_blank" rel="noreferrer"` and are validated through `safeHref()` before rendering, blocking `javascript:` injection from a compromised API response.
|
|
140
135
|
|
|
141
|
-
All GROQ queries in this plugin are static strings — no user input is interpolated
|
|
136
|
+
**GROQ queries** — All GROQ queries in this plugin are static strings — no user input is interpolated.
|
|
142
137
|
|
|
143
138
|
---
|
|
144
139
|
|
|
@@ -146,13 +141,13 @@ All GROQ queries in this plugin are static strings — no user input is interpol
|
|
|
146
141
|
|
|
147
142
|
- Sanity Studio v3, v4, or v5
|
|
148
143
|
- React 18 or 19
|
|
149
|
-
- A Vercel account with at least one project
|
|
144
|
+
- A Vercel account with at least one project and a deploy hook configured
|
|
150
145
|
|
|
151
146
|
---
|
|
152
147
|
|
|
153
148
|
## Contributing
|
|
154
149
|
|
|
155
|
-
Issues and pull requests
|
|
150
|
+
Issues and pull requests welcome at [github.com/Liiift-Studio/Deploy-Vercel-from-Sanity](https://github.com/Liiift-Studio/Deploy-Vercel-from-Sanity).
|
|
156
151
|
|
|
157
152
|
---
|
|
158
153
|
|
package/dist/index.js
CHANGED
|
@@ -412,8 +412,20 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
412
412
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Spinner, { muted: true }),
|
|
413
413
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Badge, { tone: "caution", padding: 2, children: "Triggering\u2026" })
|
|
414
414
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatusBadge, { state: latest?.state, showSpinner: true }),
|
|
415
|
+
isActiveState(latest?.state) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
416
|
+
import_ui3.Button,
|
|
417
|
+
{
|
|
418
|
+
text: "Cancel",
|
|
419
|
+
mode: "ghost",
|
|
420
|
+
tone: "critical",
|
|
421
|
+
loading: canceling,
|
|
422
|
+
disabled: canceling,
|
|
423
|
+
onClick: cancel,
|
|
424
|
+
style: { cursor: "pointer" }
|
|
425
|
+
}
|
|
426
|
+
),
|
|
415
427
|
isActive && elapsed > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { align: "center", gap: 1, children: [
|
|
416
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
428
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Spinner, { muted: true }),
|
|
417
429
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, children: formatDuration(elapsed) })
|
|
418
430
|
] }) : !isActive && deployedAt ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, children: deployedAt }) : null
|
|
419
431
|
] })
|
|
@@ -640,44 +652,30 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
640
652
|
] }) })
|
|
641
653
|
] })
|
|
642
654
|
] }),
|
|
643
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.
|
|
655
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
644
656
|
import_ui3.Flex,
|
|
645
657
|
{
|
|
646
658
|
direction: "column",
|
|
647
659
|
gap: 2,
|
|
648
660
|
className: "dvfs-deploy-col",
|
|
649
661
|
style: { flexShrink: 0, alignSelf: "stretch" },
|
|
650
|
-
children:
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
import_ui3.Button,
|
|
665
|
-
{
|
|
666
|
-
text: "Deploy",
|
|
667
|
-
tone: "primary",
|
|
668
|
-
loading: triggering,
|
|
669
|
-
disabled: isActive,
|
|
670
|
-
onClick: deploy,
|
|
671
|
-
style: {
|
|
672
|
-
flex: 1,
|
|
673
|
-
borderRadius: 0,
|
|
674
|
-
borderTopRightRadius: 3,
|
|
675
|
-
borderBottomRightRadius: 3,
|
|
676
|
-
cursor: "pointer"
|
|
677
|
-
}
|
|
662
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
663
|
+
import_ui3.Button,
|
|
664
|
+
{
|
|
665
|
+
text: "Deploy",
|
|
666
|
+
tone: "primary",
|
|
667
|
+
loading: triggering,
|
|
668
|
+
disabled: isActive,
|
|
669
|
+
onClick: deploy,
|
|
670
|
+
style: {
|
|
671
|
+
flex: 1,
|
|
672
|
+
borderRadius: 0,
|
|
673
|
+
borderTopRightRadius: 3,
|
|
674
|
+
borderBottomRightRadius: 3,
|
|
675
|
+
cursor: "pointer"
|
|
678
676
|
}
|
|
679
|
-
|
|
680
|
-
|
|
677
|
+
}
|
|
678
|
+
)
|
|
681
679
|
}
|
|
682
680
|
)
|
|
683
681
|
] }) }),
|
package/dist/index.mjs
CHANGED
|
@@ -433,8 +433,20 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
433
433
|
/* @__PURE__ */ jsx3(Spinner3, { muted: true }),
|
|
434
434
|
/* @__PURE__ */ jsx3(Badge3, { tone: "caution", padding: 2, children: "Triggering\u2026" })
|
|
435
435
|
] }) : /* @__PURE__ */ jsx3(StatusBadge, { state: latest?.state, showSpinner: true }),
|
|
436
|
+
isActiveState(latest?.state) && /* @__PURE__ */ jsx3(
|
|
437
|
+
Button2,
|
|
438
|
+
{
|
|
439
|
+
text: "Cancel",
|
|
440
|
+
mode: "ghost",
|
|
441
|
+
tone: "critical",
|
|
442
|
+
loading: canceling,
|
|
443
|
+
disabled: canceling,
|
|
444
|
+
onClick: cancel,
|
|
445
|
+
style: { cursor: "pointer" }
|
|
446
|
+
}
|
|
447
|
+
),
|
|
436
448
|
isActive && elapsed > 0 ? /* @__PURE__ */ jsxs3(Flex3, { align: "center", gap: 1, children: [
|
|
437
|
-
/* @__PURE__ */ jsx3(
|
|
449
|
+
/* @__PURE__ */ jsx3(Spinner3, { muted: true }),
|
|
438
450
|
/* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: formatDuration(elapsed) })
|
|
439
451
|
] }) : !isActive && deployedAt ? /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: deployedAt }) : null
|
|
440
452
|
] })
|
|
@@ -661,44 +673,30 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
661
673
|
] }) })
|
|
662
674
|
] })
|
|
663
675
|
] }),
|
|
664
|
-
/* @__PURE__ */
|
|
676
|
+
/* @__PURE__ */ jsx3(
|
|
665
677
|
Flex3,
|
|
666
678
|
{
|
|
667
679
|
direction: "column",
|
|
668
680
|
gap: 2,
|
|
669
681
|
className: "dvfs-deploy-col",
|
|
670
682
|
style: { flexShrink: 0, alignSelf: "stretch" },
|
|
671
|
-
children:
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
Button2,
|
|
686
|
-
{
|
|
687
|
-
text: "Deploy",
|
|
688
|
-
tone: "primary",
|
|
689
|
-
loading: triggering,
|
|
690
|
-
disabled: isActive,
|
|
691
|
-
onClick: deploy,
|
|
692
|
-
style: {
|
|
693
|
-
flex: 1,
|
|
694
|
-
borderRadius: 0,
|
|
695
|
-
borderTopRightRadius: 3,
|
|
696
|
-
borderBottomRightRadius: 3,
|
|
697
|
-
cursor: "pointer"
|
|
698
|
-
}
|
|
683
|
+
children: /* @__PURE__ */ jsx3(
|
|
684
|
+
Button2,
|
|
685
|
+
{
|
|
686
|
+
text: "Deploy",
|
|
687
|
+
tone: "primary",
|
|
688
|
+
loading: triggering,
|
|
689
|
+
disabled: isActive,
|
|
690
|
+
onClick: deploy,
|
|
691
|
+
style: {
|
|
692
|
+
flex: 1,
|
|
693
|
+
borderRadius: 0,
|
|
694
|
+
borderTopRightRadius: 3,
|
|
695
|
+
borderBottomRightRadius: 3,
|
|
696
|
+
cursor: "pointer"
|
|
699
697
|
}
|
|
700
|
-
|
|
701
|
-
|
|
698
|
+
}
|
|
699
|
+
)
|
|
702
700
|
}
|
|
703
701
|
)
|
|
704
702
|
] }) }),
|
package/package.json
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liiift-studio/deploy-vercel-from-sanity",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Sanity Studio
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Sanity Studio plugin — trigger and monitor Vercel deployments with full status, history, and build logs. Supports v3, v4, and v5.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Liiift Studio",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"sanity",
|
|
9
9
|
"sanity-plugin",
|
|
10
|
+
"sanity-studio",
|
|
10
11
|
"vercel",
|
|
12
|
+
"vercel-deploy",
|
|
11
13
|
"deploy",
|
|
12
|
-
"deployment"
|
|
14
|
+
"deployment",
|
|
15
|
+
"deploy-hook",
|
|
16
|
+
"ci-cd",
|
|
17
|
+
"build-trigger",
|
|
18
|
+
"cms-deploy"
|
|
13
19
|
],
|
|
14
20
|
"main": "./dist/index.js",
|
|
15
21
|
"module": "./dist/index.mjs",
|
|
@@ -33,7 +39,7 @@
|
|
|
33
39
|
},
|
|
34
40
|
"peerDependencies": {
|
|
35
41
|
"react": ">=18",
|
|
36
|
-
"sanity": ">=
|
|
42
|
+
"sanity": ">=3"
|
|
37
43
|
},
|
|
38
44
|
"devDependencies": {
|
|
39
45
|
"@sanity/icons": "^3",
|
|
@@ -200,9 +200,20 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
200
200
|
) : (
|
|
201
201
|
<StatusBadge state={latest?.state} showSpinner />
|
|
202
202
|
)}
|
|
203
|
+
{isActiveState(latest?.state) && (
|
|
204
|
+
<Button
|
|
205
|
+
text="Cancel"
|
|
206
|
+
mode="ghost"
|
|
207
|
+
tone="critical"
|
|
208
|
+
loading={canceling}
|
|
209
|
+
disabled={canceling}
|
|
210
|
+
onClick={cancel}
|
|
211
|
+
style={{ cursor: 'pointer' }}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
203
214
|
{isActive && elapsed > 0 ? (
|
|
204
215
|
<Flex align="center" gap={1}>
|
|
205
|
-
<
|
|
216
|
+
<Spinner muted />
|
|
206
217
|
<Text size={1} muted>{formatDuration(elapsed)}</Text>
|
|
207
218
|
</Flex>
|
|
208
219
|
) : (!isActive && deployedAt) ? (
|
|
@@ -511,17 +522,6 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
511
522
|
className="dvfs-deploy-col"
|
|
512
523
|
style={{ flexShrink: 0, alignSelf: 'stretch' }}
|
|
513
524
|
>
|
|
514
|
-
{isActiveState(latest?.state) && (
|
|
515
|
-
<Button
|
|
516
|
-
text="Cancel"
|
|
517
|
-
mode="ghost"
|
|
518
|
-
tone="critical"
|
|
519
|
-
loading={canceling}
|
|
520
|
-
disabled={canceling}
|
|
521
|
-
onClick={cancel}
|
|
522
|
-
style={{ cursor: 'pointer' }}
|
|
523
|
-
/>
|
|
524
|
-
)}
|
|
525
525
|
<Button
|
|
526
526
|
text="Deploy"
|
|
527
527
|
tone="primary"
|