@liiift-studio/deploy-vercel-from-sanity 1.3.2 → 1.4.0
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 +132 -0
- package/dist/index.d.mts +65 -2
- package/dist/index.d.ts +65 -2
- package/dist/index.js +144 -3
- package/dist/index.mjs +145 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,6 +149,7 @@ A target needs **either** `url` **or** `proxyKey`; the schema enforces that.
|
|
|
149
149
|
| `mode` | `'direct' \| 'proxy'` | `'direct'` | Transport used to reach Vercel — see [Two modes](#two-modes) |
|
|
150
150
|
| `proxyUrl` | `string` | — | Base URL of the deploy proxy, no trailing slash. Required when `mode` is `'proxy'`. |
|
|
151
151
|
| `statusKey` | `string` | — | Key sent with status requests. Must match the proxy's `VERCEL_DEPLOY_STATUS_KEY`. Ships in the Studio bundle — treat it as public. |
|
|
152
|
+
| `unblock` | `UnblockConfig` | — | Enables recovery from author-blocked deploys — see [Recovering a blocked deploy](#recovering-a-blocked-deploy). Omit to leave the feature off. |
|
|
152
153
|
|
|
153
154
|
---
|
|
154
155
|
|
|
@@ -274,6 +275,137 @@ can cancel a running build for the configured targets — see
|
|
|
274
275
|
|
|
275
276
|
---
|
|
276
277
|
|
|
278
|
+
## Recovering a blocked deploy
|
|
279
|
+
|
|
280
|
+
Vercel will not build a commit whose **git author is not a member of the team
|
|
281
|
+
that owns the project**. It creates the deployment, marks it `BLOCKED`, and
|
|
282
|
+
compiles nothing. There is no build log, because there was no build.
|
|
283
|
+
|
|
284
|
+
This is easy to miss. The deployment appears in the history like any other, and
|
|
285
|
+
before 1.4.0 the state was not in the plugin's union at all, so it rendered as
|
|
286
|
+
*Unknown* — an editor pressed **Deploy**, saw nothing go wrong, and waited for a
|
|
287
|
+
site that was never going to update.
|
|
288
|
+
|
|
289
|
+
**Pressing Deploy again cannot fix it.** The deploy hook rebuilds the current
|
|
290
|
+
HEAD, which is the same commit with the same author, so it is blocked
|
|
291
|
+
identically. The only way out is a new commit by an authorised author.
|
|
292
|
+
|
|
293
|
+
A Studio running in a browser holds no git credential, so it cannot make that
|
|
294
|
+
commit itself. With `unblock` configured it dispatches a GitHub Actions workflow
|
|
295
|
+
that does.
|
|
296
|
+
|
|
297
|
+
### The credential split
|
|
298
|
+
|
|
299
|
+
This is the part worth understanding before enabling it.
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
Studio bundle ── unblock.token Actions: write, one repo
|
|
303
|
+
│ public — anyone who can open the Studio can read it
|
|
304
|
+
▼
|
|
305
|
+
GitHub Actions ── DEPLOY_COMMIT_TOKEN Contents: write
|
|
306
|
+
│ never leaves GitHub
|
|
307
|
+
▼
|
|
308
|
+
commit as an authorised author ──▶ Vercel builds it
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Handing the Studio a `Contents: write` token would be far simpler and is the
|
|
312
|
+
obvious first design. Do not: the Studio bundle is served publicly, so that token
|
|
313
|
+
would let anyone who can load the Studio push arbitrary commits to the production
|
|
314
|
+
repository — and the next build would run them. The dispatch token is scoped so
|
|
315
|
+
that the worst a leak permits is *running the bump workflow*, which produces a
|
|
316
|
+
version bump and a deploy.
|
|
317
|
+
|
|
318
|
+
### What a leaked dispatch token can actually do
|
|
319
|
+
|
|
320
|
+
Be clear-eyed about this rather than filing it under "public by design":
|
|
321
|
+
|
|
322
|
+
- It **cannot** read your code, read other secrets, or push commits.
|
|
323
|
+
- It **can** run the bump workflow repeatedly — churning version commits and
|
|
324
|
+
burning Vercel build minutes.
|
|
325
|
+
- It **can**, without the branch allowlist, bump your *production* branch and so
|
|
326
|
+
force a deploy of whatever is currently on it. No attacker code is introduced —
|
|
327
|
+
it deploys already-merged commits — but an outsider should not be able to
|
|
328
|
+
trigger a production release.
|
|
329
|
+
|
|
330
|
+
The shipped workflow therefore opens with a branch allowlist, and a `concurrency`
|
|
331
|
+
group that serialises bumps per branch. Narrow the allowlist to the branches you
|
|
332
|
+
actually deploy. Rotate the token if it leaks.
|
|
333
|
+
|
|
334
|
+
### 1. Add the workflow
|
|
335
|
+
|
|
336
|
+
Copy [`docs/version-bump.yml`](docs/version-bump.yml) to
|
|
337
|
+
`.github/workflows/version-bump.yml` in the **site** repository, then set the git
|
|
338
|
+
identity in it to the account whose commits Vercel accepts, and narrow the branch
|
|
339
|
+
allowlist to the branches you deploy.
|
|
340
|
+
|
|
341
|
+
> **The workflow file must exist on the repository's default branch**, and on
|
|
342
|
+
> every branch you deploy from. GitHub resolves a dispatch against the default
|
|
343
|
+
> branch's copy, then runs the copy on the requested ref. A file present only on
|
|
344
|
+
> `staging` returns a 404 — which the plugin reports in full, since GitHub uses
|
|
345
|
+
> the same 404 for "no such workflow" and "your token cannot see this repo".
|
|
346
|
+
|
|
347
|
+
### 2. Add the commit credential
|
|
348
|
+
|
|
349
|
+
Create a fine-grained token on the account whose commits Vercel accepts, scoped
|
|
350
|
+
to **`Contents: write`** on that one repository, and save it as the repository
|
|
351
|
+
Actions secret **`DEPLOY_COMMIT_TOKEN`**.
|
|
352
|
+
|
|
353
|
+
It must not be the default `GITHUB_TOKEN`: commits made with it are authored by
|
|
354
|
+
`github-actions[bot]`, which is not a team member either, so Vercel would block
|
|
355
|
+
the bump for the same reason it blocked the original commit.
|
|
356
|
+
|
|
357
|
+
### 3. Add the dispatch token
|
|
358
|
+
|
|
359
|
+
Create a second fine-grained token scoped to **`Actions: write`** on the same
|
|
360
|
+
repository — and nothing else — and expose it to the Studio build:
|
|
361
|
+
|
|
362
|
+
```sh
|
|
363
|
+
SANITY_STUDIO_DEPLOY_UNBLOCK_GH_TOKEN=github_pat_…
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### 4. Configure the plugin
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
vercelDeploy({
|
|
370
|
+
unblock: {
|
|
371
|
+
token: process.env.SANITY_STUDIO_DEPLOY_UNBLOCK_GH_TOKEN,
|
|
372
|
+
owner: 'your-org',
|
|
373
|
+
repo: 'your-site-repo',
|
|
374
|
+
workflow: 'version-bump.yml', // optional, this is the default
|
|
375
|
+
},
|
|
376
|
+
})
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
| Field | Required | Description |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| `token` | yes | Fine-grained token, `Actions: write` on `repo` only. Ships in the Studio bundle — treat it as public. Unset hides the button. |
|
|
382
|
+
| `owner` | yes | Repository owner, e.g. `your-org` |
|
|
383
|
+
| `repo` | yes | Repository name |
|
|
384
|
+
| `workflow` | no | Workflow filename. Defaults to `version-bump.yml`. |
|
|
385
|
+
| `defaultRef` | no | Branch to bump when the blocked deployment names none. Normally unnecessary — the branch is read from the deployment being recovered. |
|
|
386
|
+
|
|
387
|
+
An `unblock` block missing `token`, `owner` or `repo` is discarded, so a
|
|
388
|
+
half-finished configuration renders no button rather than one that only errors.
|
|
389
|
+
|
|
390
|
+
### What the editor sees
|
|
391
|
+
|
|
392
|
+
The button appears only when the latest deployment is actually `BLOCKED`. It is
|
|
393
|
+
not a general "deploy harder" control, and it is deliberately not offered when
|
|
394
|
+
the plugin has nothing to target.
|
|
395
|
+
|
|
396
|
+
Pressing it reports that the bump was **requested**. That is the honest claim:
|
|
397
|
+
GitHub has accepted the dispatch, but the workflow still has to run, commit and
|
|
398
|
+
push before Vercel sees anything, so the new deployment turns up a minute or two
|
|
399
|
+
later. Polling picks it up on its own.
|
|
400
|
+
|
|
401
|
+
### The real fix
|
|
402
|
+
|
|
403
|
+
This is a recovery lever, not a cure. If the same author is blocked repeatedly,
|
|
404
|
+
add their GitHub account to the Vercel team — that removes the failure entirely.
|
|
405
|
+
Keep `unblock` for the cases you cannot prevent.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
277
409
|
## Troubleshooting
|
|
278
410
|
|
|
279
411
|
### "Vercel API 401 — token is invalid or expired"
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import * as sanity from 'sanity';
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
|
|
4
|
-
type VercelDeployState = 'QUEUED' | 'INITIALIZING' | 'BUILDING' | 'READY' | 'ERROR' | 'CANCELED'
|
|
4
|
+
type VercelDeployState = 'QUEUED' | 'INITIALIZING' | 'BUILDING' | 'READY' | 'ERROR' | 'CANCELED'
|
|
5
|
+
/**
|
|
6
|
+
* Vercel refused to build the commit because its git author is not a member of
|
|
7
|
+
* the team that owns the project. Nothing was compiled, so there are no build
|
|
8
|
+
* logs to read — the fix is a new commit by an authorised author, not a retry.
|
|
9
|
+
* Re-firing the deploy hook cannot clear it: the hook rebuilds the same HEAD.
|
|
10
|
+
*/
|
|
11
|
+
| 'BLOCKED' | 'LOADING';
|
|
5
12
|
/** A vercel_deploy document stored in the Sanity dataset */
|
|
6
13
|
interface DeployTarget {
|
|
7
14
|
_id: string;
|
|
@@ -47,6 +54,12 @@ interface VercelDeployment {
|
|
|
47
54
|
githubCommitRef?: string;
|
|
48
55
|
githubCommitSha?: string;
|
|
49
56
|
githubCommitAuthorName?: string;
|
|
57
|
+
/**
|
|
58
|
+
* GitHub login of the commit author. This — not `creator` — is what Vercel
|
|
59
|
+
* checks when deciding whether to build, so it is the value to name when a
|
|
60
|
+
* deployment comes back BLOCKED.
|
|
61
|
+
*/
|
|
62
|
+
githubCommitAuthorLogin?: string;
|
|
50
63
|
/** GitHub repo in "org/repo" format — used to construct commit links */
|
|
51
64
|
githubRepo?: string;
|
|
52
65
|
/** GitHub org slug — fallback when githubRepo is absent */
|
|
@@ -66,6 +79,50 @@ interface VercelDeployment {
|
|
|
66
79
|
* `proxy/README.md`.
|
|
67
80
|
*/
|
|
68
81
|
type VercelDeployMode = 'direct' | 'proxy';
|
|
82
|
+
/**
|
|
83
|
+
* Opt-in recovery path for deployments Vercel refuses to build because the HEAD
|
|
84
|
+
* commit's git author is not a member of the Vercel team.
|
|
85
|
+
*
|
|
86
|
+
* The Studio cannot fix this itself — the remedy is a commit by an authorised
|
|
87
|
+
* author, and a browser holds no git credential. So the button dispatches a
|
|
88
|
+
* GitHub Actions workflow, and that workflow does the commit with a token held
|
|
89
|
+
* in Actions secrets.
|
|
90
|
+
*
|
|
91
|
+
* The split matters. {@link token} ships inside the Studio bundle and must be
|
|
92
|
+
* treated as public; scope it to **Actions: write on the one repo** so the worst
|
|
93
|
+
* a leak permits is running that workflow. The credential that can actually
|
|
94
|
+
* write code stays in GitHub, where the browser never sees it.
|
|
95
|
+
*/
|
|
96
|
+
interface UnblockConfig {
|
|
97
|
+
/**
|
|
98
|
+
* Fine-grained GitHub token, scoped to `Actions: write` on {@link repo} alone.
|
|
99
|
+
*
|
|
100
|
+
* Compiled into the Studio bundle, so anyone who can load the Studio can read
|
|
101
|
+
* it and dispatch the workflow. Never give it `Contents: write` — that would
|
|
102
|
+
* let a reader push arbitrary commits to the production repo.
|
|
103
|
+
*
|
|
104
|
+
* Leave unset to hide the button entirely.
|
|
105
|
+
*/
|
|
106
|
+
token?: string;
|
|
107
|
+
/** Repository owner, e.g. `Liiift-Studio`. */
|
|
108
|
+
owner: string;
|
|
109
|
+
/** Repository name, e.g. `the-designers-foundry`. */
|
|
110
|
+
repo: string;
|
|
111
|
+
/**
|
|
112
|
+
* Workflow filename to dispatch. Defaults to `version-bump.yml`.
|
|
113
|
+
*
|
|
114
|
+
* GitHub resolves a dispatch against the workflow file **on the repository's
|
|
115
|
+
* default branch**, then runs the copy on the requested ref — so the file must
|
|
116
|
+
* exist on the default branch as well as on every branch you deploy from, or
|
|
117
|
+
* the dispatch returns 404.
|
|
118
|
+
*/
|
|
119
|
+
workflow?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Branch to bump when the blocked deployment does not name one. Normally
|
|
122
|
+
* unnecessary: the branch is read from the deployment being unblocked.
|
|
123
|
+
*/
|
|
124
|
+
defaultRef?: string;
|
|
125
|
+
}
|
|
69
126
|
/** Plugin configuration options */
|
|
70
127
|
interface VercelDeployPluginConfig {
|
|
71
128
|
/** Tool name slug shown in Studio sidebar (default: 'vercel-deploy') */
|
|
@@ -94,6 +151,12 @@ interface VercelDeployPluginConfig {
|
|
|
94
151
|
* API token never leaves the proxy.
|
|
95
152
|
*/
|
|
96
153
|
statusKey?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Enables the "Bump version" recovery button on deployments Vercel blocked for
|
|
156
|
+
* git-author reasons. Omit to leave the feature off — the button never renders
|
|
157
|
+
* and no GitHub token is required.
|
|
158
|
+
*/
|
|
159
|
+
unblock?: UnblockConfig;
|
|
97
160
|
}
|
|
98
161
|
|
|
99
162
|
declare const vercelDeploySchema: {
|
|
@@ -159,4 +222,4 @@ declare const vercelDeploySchema: {
|
|
|
159
222
|
*/
|
|
160
223
|
declare const vercelDeploy: sanity.Plugin<void | VercelDeployPluginConfig>;
|
|
161
224
|
|
|
162
|
-
export { type DeployTarget, type VercelDeployMode, type VercelDeployPluginConfig, type VercelDeployState, type VercelDeployment, vercelDeploy, vercelDeploySchema };
|
|
225
|
+
export { type DeployTarget, type UnblockConfig, type VercelDeployMode, type VercelDeployPluginConfig, type VercelDeployState, type VercelDeployment, vercelDeploy, vercelDeploySchema };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import * as sanity from 'sanity';
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
|
|
4
|
-
type VercelDeployState = 'QUEUED' | 'INITIALIZING' | 'BUILDING' | 'READY' | 'ERROR' | 'CANCELED'
|
|
4
|
+
type VercelDeployState = 'QUEUED' | 'INITIALIZING' | 'BUILDING' | 'READY' | 'ERROR' | 'CANCELED'
|
|
5
|
+
/**
|
|
6
|
+
* Vercel refused to build the commit because its git author is not a member of
|
|
7
|
+
* the team that owns the project. Nothing was compiled, so there are no build
|
|
8
|
+
* logs to read — the fix is a new commit by an authorised author, not a retry.
|
|
9
|
+
* Re-firing the deploy hook cannot clear it: the hook rebuilds the same HEAD.
|
|
10
|
+
*/
|
|
11
|
+
| 'BLOCKED' | 'LOADING';
|
|
5
12
|
/** A vercel_deploy document stored in the Sanity dataset */
|
|
6
13
|
interface DeployTarget {
|
|
7
14
|
_id: string;
|
|
@@ -47,6 +54,12 @@ interface VercelDeployment {
|
|
|
47
54
|
githubCommitRef?: string;
|
|
48
55
|
githubCommitSha?: string;
|
|
49
56
|
githubCommitAuthorName?: string;
|
|
57
|
+
/**
|
|
58
|
+
* GitHub login of the commit author. This — not `creator` — is what Vercel
|
|
59
|
+
* checks when deciding whether to build, so it is the value to name when a
|
|
60
|
+
* deployment comes back BLOCKED.
|
|
61
|
+
*/
|
|
62
|
+
githubCommitAuthorLogin?: string;
|
|
50
63
|
/** GitHub repo in "org/repo" format — used to construct commit links */
|
|
51
64
|
githubRepo?: string;
|
|
52
65
|
/** GitHub org slug — fallback when githubRepo is absent */
|
|
@@ -66,6 +79,50 @@ interface VercelDeployment {
|
|
|
66
79
|
* `proxy/README.md`.
|
|
67
80
|
*/
|
|
68
81
|
type VercelDeployMode = 'direct' | 'proxy';
|
|
82
|
+
/**
|
|
83
|
+
* Opt-in recovery path for deployments Vercel refuses to build because the HEAD
|
|
84
|
+
* commit's git author is not a member of the Vercel team.
|
|
85
|
+
*
|
|
86
|
+
* The Studio cannot fix this itself — the remedy is a commit by an authorised
|
|
87
|
+
* author, and a browser holds no git credential. So the button dispatches a
|
|
88
|
+
* GitHub Actions workflow, and that workflow does the commit with a token held
|
|
89
|
+
* in Actions secrets.
|
|
90
|
+
*
|
|
91
|
+
* The split matters. {@link token} ships inside the Studio bundle and must be
|
|
92
|
+
* treated as public; scope it to **Actions: write on the one repo** so the worst
|
|
93
|
+
* a leak permits is running that workflow. The credential that can actually
|
|
94
|
+
* write code stays in GitHub, where the browser never sees it.
|
|
95
|
+
*/
|
|
96
|
+
interface UnblockConfig {
|
|
97
|
+
/**
|
|
98
|
+
* Fine-grained GitHub token, scoped to `Actions: write` on {@link repo} alone.
|
|
99
|
+
*
|
|
100
|
+
* Compiled into the Studio bundle, so anyone who can load the Studio can read
|
|
101
|
+
* it and dispatch the workflow. Never give it `Contents: write` — that would
|
|
102
|
+
* let a reader push arbitrary commits to the production repo.
|
|
103
|
+
*
|
|
104
|
+
* Leave unset to hide the button entirely.
|
|
105
|
+
*/
|
|
106
|
+
token?: string;
|
|
107
|
+
/** Repository owner, e.g. `Liiift-Studio`. */
|
|
108
|
+
owner: string;
|
|
109
|
+
/** Repository name, e.g. `the-designers-foundry`. */
|
|
110
|
+
repo: string;
|
|
111
|
+
/**
|
|
112
|
+
* Workflow filename to dispatch. Defaults to `version-bump.yml`.
|
|
113
|
+
*
|
|
114
|
+
* GitHub resolves a dispatch against the workflow file **on the repository's
|
|
115
|
+
* default branch**, then runs the copy on the requested ref — so the file must
|
|
116
|
+
* exist on the default branch as well as on every branch you deploy from, or
|
|
117
|
+
* the dispatch returns 404.
|
|
118
|
+
*/
|
|
119
|
+
workflow?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Branch to bump when the blocked deployment does not name one. Normally
|
|
122
|
+
* unnecessary: the branch is read from the deployment being unblocked.
|
|
123
|
+
*/
|
|
124
|
+
defaultRef?: string;
|
|
125
|
+
}
|
|
69
126
|
/** Plugin configuration options */
|
|
70
127
|
interface VercelDeployPluginConfig {
|
|
71
128
|
/** Tool name slug shown in Studio sidebar (default: 'vercel-deploy') */
|
|
@@ -94,6 +151,12 @@ interface VercelDeployPluginConfig {
|
|
|
94
151
|
* API token never leaves the proxy.
|
|
95
152
|
*/
|
|
96
153
|
statusKey?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Enables the "Bump version" recovery button on deployments Vercel blocked for
|
|
156
|
+
* git-author reasons. Omit to leave the feature off — the button never renders
|
|
157
|
+
* and no GitHub token is required.
|
|
158
|
+
*/
|
|
159
|
+
unblock?: UnblockConfig;
|
|
97
160
|
}
|
|
98
161
|
|
|
99
162
|
declare const vercelDeploySchema: {
|
|
@@ -159,4 +222,4 @@ declare const vercelDeploySchema: {
|
|
|
159
222
|
*/
|
|
160
223
|
declare const vercelDeploy: sanity.Plugin<void | VercelDeployPluginConfig>;
|
|
161
224
|
|
|
162
|
-
export { type DeployTarget, type VercelDeployMode, type VercelDeployPluginConfig, type VercelDeployState, type VercelDeployment, vercelDeploy, vercelDeploySchema };
|
|
225
|
+
export { type DeployTarget, type UnblockConfig, type VercelDeployMode, type VercelDeployPluginConfig, type VercelDeployState, type VercelDeployment, vercelDeploy, vercelDeploySchema };
|
package/dist/index.js
CHANGED
|
@@ -108,7 +108,10 @@ function resolveConfig(options) {
|
|
|
108
108
|
mode: config.mode ?? "direct",
|
|
109
109
|
// Trailing slashes would double up when request paths are appended.
|
|
110
110
|
proxyUrl: config.proxyUrl?.replace(/\/+$/, ""),
|
|
111
|
-
statusKey: config.statusKey
|
|
111
|
+
statusKey: config.statusKey,
|
|
112
|
+
// Dropped unless it can actually be used. A config missing the token, the owner
|
|
113
|
+
// or the repo would otherwise render a button whose only outcome is an error.
|
|
114
|
+
unblock: config.unblock?.token && config.unblock.owner && config.unblock.repo ? config.unblock : void 0
|
|
112
115
|
};
|
|
113
116
|
}
|
|
114
117
|
function ConfigProvider({ value, children }) {
|
|
@@ -701,6 +704,62 @@ async function fetchDeploymentEvents(transport, target, deploymentId) {
|
|
|
701
704
|
return data.events ?? [];
|
|
702
705
|
}
|
|
703
706
|
|
|
707
|
+
// src/lib/github.ts
|
|
708
|
+
var GITHUB_API = "https://api.github.com";
|
|
709
|
+
var DEFAULT_WORKFLOW = "version-bump.yml";
|
|
710
|
+
var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
711
|
+
var REF_RE = /^[A-Za-z0-9._\-/]+$/;
|
|
712
|
+
function isValidRef(ref) {
|
|
713
|
+
if (!ref || ref.length > 255) return false;
|
|
714
|
+
if (!REF_RE.test(ref)) return false;
|
|
715
|
+
if (ref.includes("..") || ref.includes("//")) return false;
|
|
716
|
+
if (ref.startsWith("/") || ref.endsWith("/")) return false;
|
|
717
|
+
if (ref.startsWith("-") || ref.endsWith(".lock")) return false;
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
async function dispatchVersionBump(opts) {
|
|
721
|
+
const { config, ref, requestedBy } = opts;
|
|
722
|
+
const workflow = config.workflow ?? DEFAULT_WORKFLOW;
|
|
723
|
+
if (!config.token) throw new Error("No GitHub token is configured for deploy recovery.");
|
|
724
|
+
if (!SEGMENT_RE.test(config.owner)) throw new Error(`Invalid repository owner "${config.owner}".`);
|
|
725
|
+
if (!SEGMENT_RE.test(config.repo)) throw new Error(`Invalid repository name "${config.repo}".`);
|
|
726
|
+
if (!SEGMENT_RE.test(workflow)) throw new Error(`Invalid workflow filename "${workflow}".`);
|
|
727
|
+
if (!isValidRef(ref)) throw new Error(`Invalid branch name "${ref}".`);
|
|
728
|
+
const path = `/repos/${encodeURIComponent(config.owner)}/${encodeURIComponent(config.repo)}/actions/workflows/${encodeURIComponent(workflow)}/dispatches`;
|
|
729
|
+
const res = await fetch(`${GITHUB_API}${path}`, {
|
|
730
|
+
method: "POST",
|
|
731
|
+
headers: {
|
|
732
|
+
Authorization: `Bearer ${config.token}`,
|
|
733
|
+
Accept: "application/vnd.github+json",
|
|
734
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
735
|
+
"Content-Type": "application/json"
|
|
736
|
+
},
|
|
737
|
+
body: JSON.stringify({
|
|
738
|
+
ref,
|
|
739
|
+
// Workflow inputs are strings; anything else is rejected with a 422.
|
|
740
|
+
inputs: { requested_by: (requestedBy ?? "a Studio user").slice(0, 80) }
|
|
741
|
+
})
|
|
742
|
+
});
|
|
743
|
+
if (res.status === 204) return;
|
|
744
|
+
throw new Error(`${dispatchErrorMessage(res.status, workflow, ref)} (GitHub ${res.status})`);
|
|
745
|
+
}
|
|
746
|
+
function dispatchErrorMessage(status, workflow, ref) {
|
|
747
|
+
switch (status) {
|
|
748
|
+
case 401:
|
|
749
|
+
return "The GitHub token is invalid or expired. Ask a developer to reissue it.";
|
|
750
|
+
case 403:
|
|
751
|
+
return "The GitHub token is not allowed to run workflows on this repository. It needs Actions: write.";
|
|
752
|
+
case 404:
|
|
753
|
+
return `GitHub could not find "${workflow}". It must exist on the repository's default branch as well as on "${ref}" \u2014 or the token cannot see this repository.`;
|
|
754
|
+
case 422:
|
|
755
|
+
return `GitHub rejected the request: branch "${ref}" may not exist, or "${workflow}" has no workflow_dispatch trigger.`;
|
|
756
|
+
case 429:
|
|
757
|
+
return "GitHub rate limit reached. Wait a minute and try again.";
|
|
758
|
+
default:
|
|
759
|
+
return status >= 500 ? "GitHub is having problems. Try again shortly." : "The version bump could not be started.";
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
704
763
|
// src/components/DeployItem.tsx
|
|
705
764
|
var import_sanity = require("sanity");
|
|
706
765
|
|
|
@@ -769,6 +828,11 @@ function stateLabel(state) {
|
|
|
769
828
|
return { label: "Initializing", tone: "caution" };
|
|
770
829
|
case "ERROR":
|
|
771
830
|
return { label: "Error", tone: "critical" };
|
|
831
|
+
// Distinct from Error: nothing was built, so there is no log to read and a
|
|
832
|
+
// retry changes nothing. Labelling it 'Unknown' is what made this failure
|
|
833
|
+
// invisible to editors, who saw a deploy that simply never arrived.
|
|
834
|
+
case "BLOCKED":
|
|
835
|
+
return { label: "Blocked", tone: "critical" };
|
|
772
836
|
case "CANCELED":
|
|
773
837
|
return { label: "Canceled", tone: "default" };
|
|
774
838
|
case "LOADING":
|
|
@@ -916,6 +980,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
916
980
|
const toast = useToast();
|
|
917
981
|
const pluginConfig = usePluginConfig();
|
|
918
982
|
const client = (0, import_sanity.useClient)({ apiVersion: "2025-01-01" });
|
|
983
|
+
const currentUser = (0, import_sanity.useCurrentUser)();
|
|
919
984
|
const transport = (0, import_react8.useMemo)(
|
|
920
985
|
() => pluginConfig.mode === "proxy" ? { mode: "proxy", proxyUrl: pluginConfig.proxyUrl ?? "", statusKey: pluginConfig.statusKey } : { mode: "direct", token },
|
|
921
986
|
[pluginConfig.mode, pluginConfig.proxyUrl, pluginConfig.statusKey, token]
|
|
@@ -938,6 +1003,8 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
938
1003
|
const [loadingLogs, setLoadingLogs] = (0, import_react8.useState)(false);
|
|
939
1004
|
const [logError, setLogError] = (0, import_react8.useState)(null);
|
|
940
1005
|
const [pollError, setPollError] = (0, import_react8.useState)(null);
|
|
1006
|
+
const [bumping, setBumping] = (0, import_react8.useState)(false);
|
|
1007
|
+
const [bumpResult, setBumpResult] = (0, import_react8.useState)(null);
|
|
941
1008
|
const triggeredFromUidRef = (0, import_react8.useRef)(void 0);
|
|
942
1009
|
const requestSeqRef = (0, import_react8.useRef)(0);
|
|
943
1010
|
const mountedRef = (0, import_react8.useRef)(true);
|
|
@@ -1084,6 +1151,35 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
1084
1151
|
if (!showErrorLogs && errorLines.length === 0 && !logError) fetchErrorLogs();
|
|
1085
1152
|
setShowErrorLogs((v) => !v);
|
|
1086
1153
|
}, [showErrorLogs, errorLines.length, logError, fetchErrorLogs]);
|
|
1154
|
+
const requestBump = (0, import_react8.useCallback)(async () => {
|
|
1155
|
+
const unblockConfig = pluginConfig.unblock;
|
|
1156
|
+
const ref = latest?.meta?.githubCommitRef ?? unblockConfig?.defaultRef;
|
|
1157
|
+
if (!unblockConfig || !ref) return;
|
|
1158
|
+
setBumping(true);
|
|
1159
|
+
setBumpResult(null);
|
|
1160
|
+
try {
|
|
1161
|
+
await dispatchVersionBump({
|
|
1162
|
+
config: unblockConfig,
|
|
1163
|
+
ref,
|
|
1164
|
+
requestedBy: currentUser?.name || currentUser?.email || void 0
|
|
1165
|
+
});
|
|
1166
|
+
setBumpResult({
|
|
1167
|
+
ok: true,
|
|
1168
|
+
message: `Version bump requested on ${ref}. The new deploy appears here in a minute or two.`
|
|
1169
|
+
});
|
|
1170
|
+
toast.push({
|
|
1171
|
+
status: "success",
|
|
1172
|
+
title: "Version bump requested",
|
|
1173
|
+
description: `${target.name} will redeploy once the bump commit lands on ${ref}.`
|
|
1174
|
+
});
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
const message = err instanceof Error ? err.message : "The version bump could not be started.";
|
|
1177
|
+
setBumpResult({ ok: false, message });
|
|
1178
|
+
toast.push({ status: "error", title: "Could not request a version bump", description: message });
|
|
1179
|
+
} finally {
|
|
1180
|
+
setBumping(false);
|
|
1181
|
+
}
|
|
1182
|
+
}, [pluginConfig.unblock, latest?.meta?.githubCommitRef, currentUser, target.name, toast]);
|
|
1087
1183
|
const branch = latest?.meta?.githubCommitRef;
|
|
1088
1184
|
const commitMsg = latest?.meta?.githubCommitMessage?.split("\n")[0];
|
|
1089
1185
|
const sha = shortSha(latest?.meta?.githubCommitSha);
|
|
@@ -1093,6 +1189,10 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
1093
1189
|
const deployedAt = latest?.created ? timeAgo(latest.created) : null;
|
|
1094
1190
|
const vercelProjectUrl = projectHref(latest?.inspectorUrl);
|
|
1095
1191
|
const isError = latest?.state === "ERROR";
|
|
1192
|
+
const isBlocked = latest?.state === "BLOCKED";
|
|
1193
|
+
const blockedAuthor = latest?.meta?.githubCommitAuthorLogin ?? latest?.meta?.githubCommitAuthorName;
|
|
1194
|
+
const bumpRef = branch ?? pluginConfig.unblock?.defaultRef;
|
|
1195
|
+
const canUnblock = Boolean(isBlocked && pluginConfig.unblock && bumpRef);
|
|
1096
1196
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
|
|
1097
1197
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Card, { radius: 2, shadow: 1, tone: "default", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Flex, { align: "stretch", className: "dvfs-card-flex", children: [
|
|
1098
1198
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Flex, { direction: "column", flex: 1, style: { minWidth: 0 }, children: [
|
|
@@ -1256,7 +1356,48 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
1256
1356
|
] })
|
|
1257
1357
|
] }),
|
|
1258
1358
|
deployError && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Text, { size: 1, children: deployError }) })
|
|
1259
|
-
] })
|
|
1359
|
+
] }),
|
|
1360
|
+
isBlocked && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Card, { tone: "critical", padding: 3, radius: 2, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Stack, { space: 3, children: [
|
|
1361
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Flex, { align: "flex-start", gap: 2, children: [
|
|
1362
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Box, { style: { flexShrink: 0, marginTop: 2 }, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(WarningOutlineIcon, { "aria-hidden": "true" }) }),
|
|
1363
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Stack, { space: 2, children: [
|
|
1364
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Text, { size: 1, weight: "semibold", children: "Vercel refused to build this commit" }),
|
|
1365
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Text, { size: 1, children: [
|
|
1366
|
+
blockedAuthor ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
|
|
1367
|
+
"The last commit was authored by ",
|
|
1368
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: blockedAuthor }),
|
|
1369
|
+
", who is not a member of the Vercel team, so the build never started."
|
|
1370
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_jsx_runtime10.Fragment, { children: "The last commit's author is not a member of the Vercel team, so the build never started." }),
|
|
1371
|
+
" ",
|
|
1372
|
+
"Deploying again will not help \u2014 it rebuilds the same commit."
|
|
1373
|
+
] })
|
|
1374
|
+
] })
|
|
1375
|
+
] }),
|
|
1376
|
+
canUnblock ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Stack, { space: 2, children: [
|
|
1377
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1378
|
+
Button,
|
|
1379
|
+
{
|
|
1380
|
+
text: bumping ? "Requesting\u2026" : "Bump version and redeploy",
|
|
1381
|
+
tone: "critical",
|
|
1382
|
+
icon: RocketIcon,
|
|
1383
|
+
fontSize: 1,
|
|
1384
|
+
loading: bumping,
|
|
1385
|
+
disabled: bumping || bumpResult?.ok === true,
|
|
1386
|
+
onClick: requestBump,
|
|
1387
|
+
style: { alignSelf: "flex-start", cursor: "pointer" }
|
|
1388
|
+
}
|
|
1389
|
+
),
|
|
1390
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Text, { size: 0, muted: true, children: [
|
|
1391
|
+
"Adds an authorised version-bump commit on ",
|
|
1392
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: bumpRef }),
|
|
1393
|
+
", which Vercel will build."
|
|
1394
|
+
] })
|
|
1395
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Text, { size: 0, muted: true, children: pluginConfig.unblock ? "This deployment carries no branch information, so a version bump cannot be targeted. Ask a developer to deploy manually." : "Ask a developer to push a version bump \u2014 a commit by an authorised author is needed before this site can deploy." }),
|
|
1396
|
+
bumpResult && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Text, { size: 0, weight: bumpResult.ok ? "semibold" : void 0, children: [
|
|
1397
|
+
bumpResult.ok ? "\u2713 " : "",
|
|
1398
|
+
bumpResult.message
|
|
1399
|
+
] })
|
|
1400
|
+
] }) })
|
|
1260
1401
|
] }),
|
|
1261
1402
|
pollError && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Card, { tone: "caution", padding: 3, radius: 2, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Flex, { align: "center", gap: 2, children: [
|
|
1262
1403
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(WarningOutlineIcon, { "aria-hidden": "true" }),
|
|
@@ -1644,7 +1785,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
1644
1785
|
}
|
|
1645
1786
|
|
|
1646
1787
|
// src/version.ts
|
|
1647
|
-
var VERSION = "1.
|
|
1788
|
+
var VERSION = "1.4.0";
|
|
1648
1789
|
|
|
1649
1790
|
// src/components/DeployTool.tsx
|
|
1650
1791
|
var import_jsx_runtime13 = require("react/jsx-runtime");
|
package/dist/index.mjs
CHANGED
|
@@ -73,7 +73,10 @@ function resolveConfig(options) {
|
|
|
73
73
|
mode: config.mode ?? "direct",
|
|
74
74
|
// Trailing slashes would double up when request paths are appended.
|
|
75
75
|
proxyUrl: config.proxyUrl?.replace(/\/+$/, ""),
|
|
76
|
-
statusKey: config.statusKey
|
|
76
|
+
statusKey: config.statusKey,
|
|
77
|
+
// Dropped unless it can actually be used. A config missing the token, the owner
|
|
78
|
+
// or the repo would otherwise render a button whose only outcome is an error.
|
|
79
|
+
unblock: config.unblock?.token && config.unblock.owner && config.unblock.repo ? config.unblock : void 0
|
|
77
80
|
};
|
|
78
81
|
}
|
|
79
82
|
function ConfigProvider({ value, children }) {
|
|
@@ -666,8 +669,64 @@ async function fetchDeploymentEvents(transport, target, deploymentId) {
|
|
|
666
669
|
return data.events ?? [];
|
|
667
670
|
}
|
|
668
671
|
|
|
672
|
+
// src/lib/github.ts
|
|
673
|
+
var GITHUB_API = "https://api.github.com";
|
|
674
|
+
var DEFAULT_WORKFLOW = "version-bump.yml";
|
|
675
|
+
var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
676
|
+
var REF_RE = /^[A-Za-z0-9._\-/]+$/;
|
|
677
|
+
function isValidRef(ref) {
|
|
678
|
+
if (!ref || ref.length > 255) return false;
|
|
679
|
+
if (!REF_RE.test(ref)) return false;
|
|
680
|
+
if (ref.includes("..") || ref.includes("//")) return false;
|
|
681
|
+
if (ref.startsWith("/") || ref.endsWith("/")) return false;
|
|
682
|
+
if (ref.startsWith("-") || ref.endsWith(".lock")) return false;
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
async function dispatchVersionBump(opts) {
|
|
686
|
+
const { config, ref, requestedBy } = opts;
|
|
687
|
+
const workflow = config.workflow ?? DEFAULT_WORKFLOW;
|
|
688
|
+
if (!config.token) throw new Error("No GitHub token is configured for deploy recovery.");
|
|
689
|
+
if (!SEGMENT_RE.test(config.owner)) throw new Error(`Invalid repository owner "${config.owner}".`);
|
|
690
|
+
if (!SEGMENT_RE.test(config.repo)) throw new Error(`Invalid repository name "${config.repo}".`);
|
|
691
|
+
if (!SEGMENT_RE.test(workflow)) throw new Error(`Invalid workflow filename "${workflow}".`);
|
|
692
|
+
if (!isValidRef(ref)) throw new Error(`Invalid branch name "${ref}".`);
|
|
693
|
+
const path = `/repos/${encodeURIComponent(config.owner)}/${encodeURIComponent(config.repo)}/actions/workflows/${encodeURIComponent(workflow)}/dispatches`;
|
|
694
|
+
const res = await fetch(`${GITHUB_API}${path}`, {
|
|
695
|
+
method: "POST",
|
|
696
|
+
headers: {
|
|
697
|
+
Authorization: `Bearer ${config.token}`,
|
|
698
|
+
Accept: "application/vnd.github+json",
|
|
699
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
700
|
+
"Content-Type": "application/json"
|
|
701
|
+
},
|
|
702
|
+
body: JSON.stringify({
|
|
703
|
+
ref,
|
|
704
|
+
// Workflow inputs are strings; anything else is rejected with a 422.
|
|
705
|
+
inputs: { requested_by: (requestedBy ?? "a Studio user").slice(0, 80) }
|
|
706
|
+
})
|
|
707
|
+
});
|
|
708
|
+
if (res.status === 204) return;
|
|
709
|
+
throw new Error(`${dispatchErrorMessage(res.status, workflow, ref)} (GitHub ${res.status})`);
|
|
710
|
+
}
|
|
711
|
+
function dispatchErrorMessage(status, workflow, ref) {
|
|
712
|
+
switch (status) {
|
|
713
|
+
case 401:
|
|
714
|
+
return "The GitHub token is invalid or expired. Ask a developer to reissue it.";
|
|
715
|
+
case 403:
|
|
716
|
+
return "The GitHub token is not allowed to run workflows on this repository. It needs Actions: write.";
|
|
717
|
+
case 404:
|
|
718
|
+
return `GitHub could not find "${workflow}". It must exist on the repository's default branch as well as on "${ref}" \u2014 or the token cannot see this repository.`;
|
|
719
|
+
case 422:
|
|
720
|
+
return `GitHub rejected the request: branch "${ref}" may not exist, or "${workflow}" has no workflow_dispatch trigger.`;
|
|
721
|
+
case 429:
|
|
722
|
+
return "GitHub rate limit reached. Wait a minute and try again.";
|
|
723
|
+
default:
|
|
724
|
+
return status >= 500 ? "GitHub is having problems. Try again shortly." : "The version bump could not be started.";
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
669
728
|
// src/components/DeployItem.tsx
|
|
670
|
-
import { useClient } from "sanity";
|
|
729
|
+
import { useClient, useCurrentUser } from "sanity";
|
|
671
730
|
|
|
672
731
|
// src/lib/helpers.ts
|
|
673
732
|
function parseHookUrl(url) {
|
|
@@ -734,6 +793,11 @@ function stateLabel(state) {
|
|
|
734
793
|
return { label: "Initializing", tone: "caution" };
|
|
735
794
|
case "ERROR":
|
|
736
795
|
return { label: "Error", tone: "critical" };
|
|
796
|
+
// Distinct from Error: nothing was built, so there is no log to read and a
|
|
797
|
+
// retry changes nothing. Labelling it 'Unknown' is what made this failure
|
|
798
|
+
// invisible to editors, who saw a deploy that simply never arrived.
|
|
799
|
+
case "BLOCKED":
|
|
800
|
+
return { label: "Blocked", tone: "critical" };
|
|
737
801
|
case "CANCELED":
|
|
738
802
|
return { label: "Canceled", tone: "default" };
|
|
739
803
|
case "LOADING":
|
|
@@ -881,6 +945,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
881
945
|
const toast = useToast();
|
|
882
946
|
const pluginConfig = usePluginConfig();
|
|
883
947
|
const client = useClient({ apiVersion: "2025-01-01" });
|
|
948
|
+
const currentUser = useCurrentUser();
|
|
884
949
|
const transport = useMemo2(
|
|
885
950
|
() => pluginConfig.mode === "proxy" ? { mode: "proxy", proxyUrl: pluginConfig.proxyUrl ?? "", statusKey: pluginConfig.statusKey } : { mode: "direct", token },
|
|
886
951
|
[pluginConfig.mode, pluginConfig.proxyUrl, pluginConfig.statusKey, token]
|
|
@@ -903,6 +968,8 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
903
968
|
const [loadingLogs, setLoadingLogs] = useState5(false);
|
|
904
969
|
const [logError, setLogError] = useState5(null);
|
|
905
970
|
const [pollError, setPollError] = useState5(null);
|
|
971
|
+
const [bumping, setBumping] = useState5(false);
|
|
972
|
+
const [bumpResult, setBumpResult] = useState5(null);
|
|
906
973
|
const triggeredFromUidRef = useRef2(void 0);
|
|
907
974
|
const requestSeqRef = useRef2(0);
|
|
908
975
|
const mountedRef = useRef2(true);
|
|
@@ -1049,6 +1116,35 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
1049
1116
|
if (!showErrorLogs && errorLines.length === 0 && !logError) fetchErrorLogs();
|
|
1050
1117
|
setShowErrorLogs((v) => !v);
|
|
1051
1118
|
}, [showErrorLogs, errorLines.length, logError, fetchErrorLogs]);
|
|
1119
|
+
const requestBump = useCallback4(async () => {
|
|
1120
|
+
const unblockConfig = pluginConfig.unblock;
|
|
1121
|
+
const ref = latest?.meta?.githubCommitRef ?? unblockConfig?.defaultRef;
|
|
1122
|
+
if (!unblockConfig || !ref) return;
|
|
1123
|
+
setBumping(true);
|
|
1124
|
+
setBumpResult(null);
|
|
1125
|
+
try {
|
|
1126
|
+
await dispatchVersionBump({
|
|
1127
|
+
config: unblockConfig,
|
|
1128
|
+
ref,
|
|
1129
|
+
requestedBy: currentUser?.name || currentUser?.email || void 0
|
|
1130
|
+
});
|
|
1131
|
+
setBumpResult({
|
|
1132
|
+
ok: true,
|
|
1133
|
+
message: `Version bump requested on ${ref}. The new deploy appears here in a minute or two.`
|
|
1134
|
+
});
|
|
1135
|
+
toast.push({
|
|
1136
|
+
status: "success",
|
|
1137
|
+
title: "Version bump requested",
|
|
1138
|
+
description: `${target.name} will redeploy once the bump commit lands on ${ref}.`
|
|
1139
|
+
});
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
const message = err instanceof Error ? err.message : "The version bump could not be started.";
|
|
1142
|
+
setBumpResult({ ok: false, message });
|
|
1143
|
+
toast.push({ status: "error", title: "Could not request a version bump", description: message });
|
|
1144
|
+
} finally {
|
|
1145
|
+
setBumping(false);
|
|
1146
|
+
}
|
|
1147
|
+
}, [pluginConfig.unblock, latest?.meta?.githubCommitRef, currentUser, target.name, toast]);
|
|
1052
1148
|
const branch = latest?.meta?.githubCommitRef;
|
|
1053
1149
|
const commitMsg = latest?.meta?.githubCommitMessage?.split("\n")[0];
|
|
1054
1150
|
const sha = shortSha(latest?.meta?.githubCommitSha);
|
|
@@ -1058,6 +1154,10 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
1058
1154
|
const deployedAt = latest?.created ? timeAgo(latest.created) : null;
|
|
1059
1155
|
const vercelProjectUrl = projectHref(latest?.inspectorUrl);
|
|
1060
1156
|
const isError = latest?.state === "ERROR";
|
|
1157
|
+
const isBlocked = latest?.state === "BLOCKED";
|
|
1158
|
+
const blockedAuthor = latest?.meta?.githubCommitAuthorLogin ?? latest?.meta?.githubCommitAuthorName;
|
|
1159
|
+
const bumpRef = branch ?? pluginConfig.unblock?.defaultRef;
|
|
1160
|
+
const canUnblock = Boolean(isBlocked && pluginConfig.unblock && bumpRef);
|
|
1061
1161
|
return /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
1062
1162
|
/* @__PURE__ */ jsx10(Card, { radius: 2, shadow: 1, tone: "default", children: /* @__PURE__ */ jsxs6(Flex, { align: "stretch", className: "dvfs-card-flex", children: [
|
|
1063
1163
|
/* @__PURE__ */ jsxs6(Flex, { direction: "column", flex: 1, style: { minWidth: 0 }, children: [
|
|
@@ -1221,7 +1321,48 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
1221
1321
|
] })
|
|
1222
1322
|
] }),
|
|
1223
1323
|
deployError && /* @__PURE__ */ jsx10(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsx10(Text, { size: 1, children: deployError }) })
|
|
1224
|
-
] })
|
|
1324
|
+
] }),
|
|
1325
|
+
isBlocked && /* @__PURE__ */ jsx10(Card, { tone: "critical", padding: 3, radius: 2, children: /* @__PURE__ */ jsxs6(Stack, { space: 3, children: [
|
|
1326
|
+
/* @__PURE__ */ jsxs6(Flex, { align: "flex-start", gap: 2, children: [
|
|
1327
|
+
/* @__PURE__ */ jsx10(Box, { style: { flexShrink: 0, marginTop: 2 }, children: /* @__PURE__ */ jsx10(WarningOutlineIcon, { "aria-hidden": "true" }) }),
|
|
1328
|
+
/* @__PURE__ */ jsxs6(Stack, { space: 2, children: [
|
|
1329
|
+
/* @__PURE__ */ jsx10(Text, { size: 1, weight: "semibold", children: "Vercel refused to build this commit" }),
|
|
1330
|
+
/* @__PURE__ */ jsxs6(Text, { size: 1, children: [
|
|
1331
|
+
blockedAuthor ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
1332
|
+
"The last commit was authored by ",
|
|
1333
|
+
/* @__PURE__ */ jsx10("strong", { children: blockedAuthor }),
|
|
1334
|
+
", who is not a member of the Vercel team, so the build never started."
|
|
1335
|
+
] }) : /* @__PURE__ */ jsx10(Fragment, { children: "The last commit's author is not a member of the Vercel team, so the build never started." }),
|
|
1336
|
+
" ",
|
|
1337
|
+
"Deploying again will not help \u2014 it rebuilds the same commit."
|
|
1338
|
+
] })
|
|
1339
|
+
] })
|
|
1340
|
+
] }),
|
|
1341
|
+
canUnblock ? /* @__PURE__ */ jsxs6(Stack, { space: 2, children: [
|
|
1342
|
+
/* @__PURE__ */ jsx10(
|
|
1343
|
+
Button,
|
|
1344
|
+
{
|
|
1345
|
+
text: bumping ? "Requesting\u2026" : "Bump version and redeploy",
|
|
1346
|
+
tone: "critical",
|
|
1347
|
+
icon: RocketIcon,
|
|
1348
|
+
fontSize: 1,
|
|
1349
|
+
loading: bumping,
|
|
1350
|
+
disabled: bumping || bumpResult?.ok === true,
|
|
1351
|
+
onClick: requestBump,
|
|
1352
|
+
style: { alignSelf: "flex-start", cursor: "pointer" }
|
|
1353
|
+
}
|
|
1354
|
+
),
|
|
1355
|
+
/* @__PURE__ */ jsxs6(Text, { size: 0, muted: true, children: [
|
|
1356
|
+
"Adds an authorised version-bump commit on ",
|
|
1357
|
+
/* @__PURE__ */ jsx10("code", { children: bumpRef }),
|
|
1358
|
+
", which Vercel will build."
|
|
1359
|
+
] })
|
|
1360
|
+
] }) : /* @__PURE__ */ jsx10(Text, { size: 0, muted: true, children: pluginConfig.unblock ? "This deployment carries no branch information, so a version bump cannot be targeted. Ask a developer to deploy manually." : "Ask a developer to push a version bump \u2014 a commit by an authorised author is needed before this site can deploy." }),
|
|
1361
|
+
bumpResult && /* @__PURE__ */ jsxs6(Text, { size: 0, weight: bumpResult.ok ? "semibold" : void 0, children: [
|
|
1362
|
+
bumpResult.ok ? "\u2713 " : "",
|
|
1363
|
+
bumpResult.message
|
|
1364
|
+
] })
|
|
1365
|
+
] }) })
|
|
1225
1366
|
] }),
|
|
1226
1367
|
pollError && /* @__PURE__ */ jsx10(Card, { tone: "caution", padding: 3, radius: 2, children: /* @__PURE__ */ jsxs6(Flex, { align: "center", gap: 2, children: [
|
|
1227
1368
|
/* @__PURE__ */ jsx10(WarningOutlineIcon, { "aria-hidden": "true" }),
|
|
@@ -1609,7 +1750,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
1609
1750
|
}
|
|
1610
1751
|
|
|
1611
1752
|
// src/version.ts
|
|
1612
|
-
var VERSION = "1.
|
|
1753
|
+
var VERSION = "1.4.0";
|
|
1613
1754
|
|
|
1614
1755
|
// src/components/DeployTool.tsx
|
|
1615
1756
|
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liiift-studio/deploy-vercel-from-sanity",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Sanity Studio plugin — trigger and monitor Vercel deployments with full status, history, and build logs. Supports Studio v3.30 through v6.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Liiift Studio",
|