@orange-soft/strapi-deployment-trigger 1.1.2 → 1.2.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 CHANGED
@@ -1,12 +1,14 @@
1
1
  # Strapi Plugin: Deployment Trigger
2
2
 
3
- A Strapi v5 plugin that allows you to trigger GitHub Actions workflow deployments directly from the Strapi admin panel.
3
+ A Strapi v5 plugin that allows you to trigger deployments directly from the Strapi admin panel. Supports both **GitHub Actions** and **Vercel** deployments.
4
4
 
5
5
  ## Features
6
6
 
7
- - Trigger GitHub Actions `workflow_dispatch` events from the admin UI
8
- - Configure repository URL, workflow file, and branch
9
- - Secure token storage in database (or via environment variable)
7
+ - Trigger **GitHub Actions** `workflow_dispatch` events from the admin UI
8
+ - Trigger **Vercel** deployments via deploy hooks
9
+ - Configure multiple deployment targets (e.g., Production, Staging)
10
+ - Support for different trigger types per target
11
+ - Secure token storage in database
10
12
  - Token masking for security
11
13
  - Direct link to GitHub Actions to monitor deployment progress
12
14
 
@@ -41,12 +43,14 @@ export default () => ({
41
43
  ### 2. Configure via Admin UI
42
44
 
43
45
  1. Go to **Plugins > Deployment Trigger > Settings** in your Strapi admin
44
- 2. Enter your GitHub repository URL (e.g., `https://github.com/owner/repo`)
45
- 3. Enter the workflow filename (e.g., `deploy.yml`)
46
- 4. Enter the branch name to trigger (e.g., `main` or `master`)
47
- 5. Enter your GitHub Personal Access Token
46
+ 2. **For GitHub targets:**
47
+ - Enter your GitHub repository URL (e.g., `https://github.com/owner/repo`)
48
+ - Enter your GitHub Personal Access Token
49
+ - Add a target with type "GitHub", workflow filename, and branch
50
+ 3. **For Vercel targets:**
51
+ - Add a target with type "Vercel" and paste your deploy hook URL
48
52
 
49
- All settings including the token are stored securely in the Strapi database.
53
+ All settings including tokens are stored securely in the Strapi database.
50
54
 
51
55
  ## GitHub Token Setup
52
56
 
@@ -77,12 +81,29 @@ jobs:
77
81
  # ... your deployment steps
78
82
  ```
79
83
 
84
+ ## Vercel Deploy Hook Setup
85
+
86
+ To trigger Vercel deployments, you need to create a Deploy Hook:
87
+
88
+ 1. Go to your Vercel project dashboard
89
+ 2. Navigate to **Settings** > **Git**
90
+ 3. Scroll down to **Deploy Hooks**
91
+ 4. Click **Create Hook**
92
+ 5. Enter a name (e.g., "Strapi Trigger") and select the branch
93
+ 6. Click **Create Hook**
94
+ 7. Copy the generated webhook URL (starts with `https://api.vercel.com/v1/integrations/deploy/...`)
95
+ 8. Paste this URL when adding a Vercel target in the plugin settings
96
+
80
97
  ## Usage
81
98
 
82
- 1. Navigate to **Plugins > Deployment Trigger** in your Strapi admin
83
- 2. Verify your configuration is correct
84
- 3. Click **Trigger Deployment**
85
- 4. Monitor the deployment progress via the provided GitHub Actions link
99
+ 1. Navigate to **Plugins > Deployment Trigger > Settings** in your Strapi admin
100
+ 2. For GitHub targets: Configure repository URL and GitHub token
101
+ 3. Add deployment targets:
102
+ - **GitHub**: Select type "GitHub", enter name, workflow file, and branch
103
+ - **Vercel**: Select type "Vercel", enter name and webhook URL
104
+ 4. Go to the main **Deployment Trigger** page
105
+ 5. Click **Trigger** on any configured target
106
+ 6. For GitHub targets, click the provided link to monitor progress in GitHub Actions
86
107
 
87
108
  ## API
88
109
 
@@ -93,6 +93,20 @@ const HomePage = () => {
93
93
  const hasToken = status?.hasToken;
94
94
  const targets = settings.targets || [];
95
95
 
96
+ // Check if there are any GitHub targets that need configuration
97
+ const hasGitHubTargets = targets.some(t => (t.type || 'github') === 'github');
98
+ const hasVercelTargets = targets.some(t => t.type === 'vercel');
99
+
100
+ // Determine if trigger buttons should be enabled
101
+ // GitHub targets need repoUrl + token, Vercel targets are self-contained
102
+ const canTrigger = (target) => {
103
+ const targetType = target.type || 'github';
104
+ if (targetType === 'github') {
105
+ return hasToken && parsed.owner && parsed.repo;
106
+ }
107
+ return !!target.webhookUrl;
108
+ };
109
+
96
110
  return (
97
111
  <Layouts.Root>
98
112
  <Layouts.Header
@@ -134,7 +148,7 @@ const HomePage = () => {
134
148
  </Box>
135
149
  )}
136
150
 
137
- {!hasToken && (
151
+ {hasGitHubTargets && !hasToken && (
138
152
  <Box paddingBottom={4}>
139
153
  <Alert title="Token Missing" variant="danger">
140
154
  GitHub Personal Access Token is not configured. Please add it in Settings.
@@ -142,7 +156,7 @@ const HomePage = () => {
142
156
  </Box>
143
157
  )}
144
158
 
145
- {!settings.repoUrl && (
159
+ {hasGitHubTargets && !settings.repoUrl && (
146
160
  <Box paddingBottom={4}>
147
161
  <Alert title="Configuration Required" variant="warning">
148
162
  Please configure your GitHub repository in the Settings page before triggering deployments.
@@ -211,37 +225,50 @@ const HomePage = () => {
211
225
  <Table>
212
226
  <Thead>
213
227
  <Tr>
228
+ <Th><Typography variant="sigma">Type</Typography></Th>
214
229
  <Th><Typography variant="sigma">Name</Typography></Th>
215
- <Th><Typography variant="sigma">Workflow</Typography></Th>
216
- <Th><Typography variant="sigma">Branch</Typography></Th>
230
+ <Th><Typography variant="sigma">Details</Typography></Th>
217
231
  <Th><Typography variant="sigma">Action</Typography></Th>
218
232
  </Tr>
219
233
  </Thead>
220
234
  <Tbody>
221
- {targets.map((target) => (
222
- <Tr key={target.id}>
223
- <Td>
224
- <Typography variant="omega" fontWeight="bold">{target.name}</Typography>
225
- </Td>
226
- <Td>
227
- <Typography variant="omega">{target.workflow}</Typography>
228
- </Td>
229
- <Td>
230
- <Typography variant="omega">{target.branch}</Typography>
231
- </Td>
232
- <Td>
233
- <Button
234
- onClick={() => handleDeploy(target.id, target.name)}
235
- loading={deployingTargetId === target.id}
236
- disabled={!isConfigured || deployingTargetId !== null}
237
- startIcon={<Rocket />}
238
- size="S"
239
- >
240
- {deployingTargetId === target.id ? 'Triggering...' : 'Trigger'}
241
- </Button>
242
- </Td>
243
- </Tr>
244
- ))}
235
+ {targets.map((target) => {
236
+ const targetType = target.type || 'github';
237
+ return (
238
+ <Tr key={target.id}>
239
+ <Td>
240
+ <Typography variant="omega" fontWeight="bold" textColor={targetType === 'github' ? 'neutral800' : 'secondary600'}>
241
+ {targetType === 'github' ? 'GitHub' : 'Vercel'}
242
+ </Typography>
243
+ </Td>
244
+ <Td>
245
+ <Typography variant="omega" fontWeight="bold">{target.name}</Typography>
246
+ </Td>
247
+ <Td>
248
+ {targetType === 'github' ? (
249
+ <Typography variant="omega" textColor="neutral600">
250
+ {target.workflow} / {target.branch}
251
+ </Typography>
252
+ ) : (
253
+ <Typography variant="omega" textColor="neutral600">
254
+ Webhook
255
+ </Typography>
256
+ )}
257
+ </Td>
258
+ <Td>
259
+ <Button
260
+ onClick={() => handleDeploy(target.id, target.name)}
261
+ loading={deployingTargetId === target.id}
262
+ disabled={!canTrigger(target) || deployingTargetId !== null}
263
+ startIcon={<Rocket />}
264
+ size="S"
265
+ >
266
+ {deployingTargetId === target.id ? 'Triggering...' : 'Trigger'}
267
+ </Button>
268
+ </Td>
269
+ </Tr>
270
+ );
271
+ })}
245
272
  </Tbody>
246
273
  </Table>
247
274
  ) : (
@@ -259,8 +286,8 @@ const HomePage = () => {
259
286
  </Flex>
260
287
  </Box>
261
288
 
262
- {/* Instructions Card - Show only when not configured */}
263
- {!isConfigured && targets.length > 0 && (
289
+ {/* Instructions Card - Show only when GitHub targets need configuration */}
290
+ {hasGitHubTargets && (!hasToken || !parsed.owner || !parsed.repo) && (
264
291
  <Box
265
292
  background="neutral0"
266
293
  hasRadius
@@ -272,10 +299,10 @@ const HomePage = () => {
272
299
  >
273
300
  <Flex direction="column" alignItems="center" justifyContent="center" gap={3}>
274
301
  <Typography variant="beta" textColor="neutral600" textAlign="center">
275
- Setup Incomplete
302
+ GitHub Setup Incomplete
276
303
  </Typography>
277
304
  <Typography variant="epsilon" textColor="neutral600" textAlign="center">
278
- Please ensure repository URL and GitHub token are configured in Settings.
305
+ Please ensure repository URL and GitHub token are configured in Settings for GitHub targets.
279
306
  </Typography>
280
307
  <Box paddingTop={2}>
281
308
  <Link to={`/plugins/${PLUGIN_ID}/settings`}>
@@ -15,9 +15,10 @@ import {
15
15
  Tr,
16
16
  Th,
17
17
  Td,
18
- IconButton,
19
18
  Dialog,
20
19
  Grid,
20
+ SingleSelect,
21
+ SingleSelectOption,
21
22
  } from '@strapi/design-system';
22
23
  import { Check, Plus, Pencil, Trash } from '@strapi/icons';
23
24
 
@@ -27,6 +28,7 @@ import { PLUGIN_ID } from '../pluginId';
27
28
  const TOKEN_PATTERN = /^github_pat_[a-zA-Z0-9_]+$/;
28
29
  const REPO_URL_PATTERN = /^https:\/\/github\.com\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+\/?$/;
29
30
  const WORKFLOW_PATTERN = /^[a-zA-Z0-9_.-]+\.ya?ml$/;
31
+ const VERCEL_WEBHOOK_PATTERN = /^https:\/\/api\.vercel\.com\/v1\/integrations\/deploy\/.+$/;
30
32
 
31
33
  const validateToken = (value) => {
32
34
  if (!value) return null;
@@ -52,6 +54,14 @@ const validateWorkflow = (value) => {
52
54
  return null;
53
55
  };
54
56
 
57
+ const validateVercelWebhook = (value) => {
58
+ if (!value) return 'Webhook URL is required';
59
+ if (!VERCEL_WEBHOOK_PATTERN.test(value)) {
60
+ return 'Must be a valid Vercel deploy hook URL (https://api.vercel.com/v1/integrations/deploy/...)';
61
+ }
62
+ return null;
63
+ };
64
+
55
65
  const SettingsPage = () => {
56
66
  const navigate = useNavigate();
57
67
  const { get, put, post, del } = useFetchClient();
@@ -69,7 +79,7 @@ const SettingsPage = () => {
69
79
 
70
80
  // Target form state
71
81
  const [editingTarget, setEditingTarget] = useState(null);
72
- const [targetForm, setTargetForm] = useState({ name: '', workflow: '', branch: '' });
82
+ const [targetForm, setTargetForm] = useState({ type: 'github', name: '', workflow: '', branch: '', webhookUrl: '' });
73
83
  const [targetErrors, setTargetErrors] = useState({});
74
84
  const [showAddForm, setShowAddForm] = useState(false);
75
85
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -145,7 +155,7 @@ const SettingsPage = () => {
145
155
 
146
156
  // Target management
147
157
  const resetTargetForm = () => {
148
- setTargetForm({ name: '', workflow: 'deploy.yml', branch: 'master' });
158
+ setTargetForm({ type: 'github', name: '', workflow: 'deploy.yml', branch: 'master', webhookUrl: '' });
149
159
  setTargetErrors({});
150
160
  setEditingTarget(null);
151
161
  setShowAddForm(false);
@@ -154,9 +164,16 @@ const SettingsPage = () => {
154
164
  const validateTargetForm = () => {
155
165
  const newErrors = {};
156
166
  if (!targetForm.name.trim()) newErrors.name = 'Name is required';
157
- const workflowError = validateWorkflow(targetForm.workflow);
158
- if (workflowError) newErrors.workflow = workflowError;
159
- if (!targetForm.branch.trim()) newErrors.branch = 'Branch is required';
167
+
168
+ if (targetForm.type === 'github') {
169
+ const workflowError = validateWorkflow(targetForm.workflow);
170
+ if (workflowError) newErrors.workflow = workflowError;
171
+ if (!targetForm.branch.trim()) newErrors.branch = 'Branch is required';
172
+ } else if (targetForm.type === 'vercel') {
173
+ const webhookError = validateVercelWebhook(targetForm.webhookUrl);
174
+ if (webhookError) newErrors.webhookUrl = webhookError;
175
+ }
176
+
160
177
  setTargetErrors(newErrors);
161
178
  return Object.keys(newErrors).length === 0;
162
179
  };
@@ -180,7 +197,13 @@ const SettingsPage = () => {
180
197
 
181
198
  const handleEditTarget = (target) => {
182
199
  setEditingTarget(target.id);
183
- setTargetForm({ name: target.name, workflow: target.workflow, branch: target.branch });
200
+ setTargetForm({
201
+ type: target.type || 'github',
202
+ name: target.name,
203
+ workflow: target.workflow || 'deploy.yml',
204
+ branch: target.branch || 'master',
205
+ webhookUrl: target.webhookUrl || '',
206
+ });
184
207
  setShowAddForm(false);
185
208
  };
186
209
 
@@ -330,7 +353,7 @@ const SettingsPage = () => {
330
353
  startIcon={<Plus />}
331
354
  onClick={() => {
332
355
  setShowAddForm(true);
333
- setTargetForm({ name: '', workflow: 'deploy.yml', branch: 'master' });
356
+ setTargetForm({ type: 'github', name: '', workflow: 'deploy.yml', branch: 'master', webhookUrl: '' });
334
357
  }}
335
358
  size="S"
336
359
  >
@@ -351,7 +374,19 @@ const SettingsPage = () => {
351
374
  {editingTarget ? 'Edit Target' : 'Add New Target'}
352
375
  </Typography>
353
376
  <Grid.Root gap={4}>
354
- <Grid.Item col={4} s={12}>
377
+ <Grid.Item col={3} s={12}>
378
+ <Field.Root name="targetType" required>
379
+ <Field.Label>Type</Field.Label>
380
+ <SingleSelect
381
+ value={targetForm.type}
382
+ onChange={(value) => setTargetForm(prev => ({ ...prev, type: value }))}
383
+ >
384
+ <SingleSelectOption value="github">GitHub</SingleSelectOption>
385
+ <SingleSelectOption value="vercel">Vercel</SingleSelectOption>
386
+ </SingleSelect>
387
+ </Field.Root>
388
+ </Grid.Item>
389
+ <Grid.Item col={3} s={12}>
355
390
  <Field.Root name="targetName" required error={targetErrors.name}>
356
391
  <Field.Label>Name</Field.Label>
357
392
  <Field.Input
@@ -362,28 +397,49 @@ const SettingsPage = () => {
362
397
  <Field.Error />
363
398
  </Field.Root>
364
399
  </Grid.Item>
365
- <Grid.Item col={4} s={12}>
366
- <Field.Root name="targetWorkflow" required error={targetErrors.workflow}>
367
- <Field.Label>Workflow File</Field.Label>
368
- <Field.Input
369
- placeholder="deploy.yml"
370
- value={targetForm.workflow}
371
- onChange={handleTargetFormChange('workflow')}
372
- />
373
- <Field.Error />
374
- </Field.Root>
375
- </Grid.Item>
376
- <Grid.Item col={4} s={12}>
377
- <Field.Root name="targetBranch" required error={targetErrors.branch}>
378
- <Field.Label>Branch</Field.Label>
379
- <Field.Input
380
- placeholder="main"
381
- value={targetForm.branch}
382
- onChange={handleTargetFormChange('branch')}
383
- />
384
- <Field.Error />
385
- </Field.Root>
386
- </Grid.Item>
400
+
401
+ {/* GitHub-specific fields */}
402
+ {targetForm.type === 'github' && (
403
+ <>
404
+ <Grid.Item col={3} s={12}>
405
+ <Field.Root name="targetWorkflow" required error={targetErrors.workflow}>
406
+ <Field.Label>Workflow File</Field.Label>
407
+ <Field.Input
408
+ placeholder="deploy.yml"
409
+ value={targetForm.workflow}
410
+ onChange={handleTargetFormChange('workflow')}
411
+ />
412
+ <Field.Error />
413
+ </Field.Root>
414
+ </Grid.Item>
415
+ <Grid.Item col={3} s={12}>
416
+ <Field.Root name="targetBranch" required error={targetErrors.branch}>
417
+ <Field.Label>Branch</Field.Label>
418
+ <Field.Input
419
+ placeholder="main"
420
+ value={targetForm.branch}
421
+ onChange={handleTargetFormChange('branch')}
422
+ />
423
+ <Field.Error />
424
+ </Field.Root>
425
+ </Grid.Item>
426
+ </>
427
+ )}
428
+
429
+ {/* Vercel-specific fields */}
430
+ {targetForm.type === 'vercel' && (
431
+ <Grid.Item col={6} s={12}>
432
+ <Field.Root name="targetWebhookUrl" required error={targetErrors.webhookUrl}>
433
+ <Field.Label>Webhook URL</Field.Label>
434
+ <Field.Input
435
+ placeholder="https://api.vercel.com/v1/integrations/deploy/..."
436
+ value={targetForm.webhookUrl}
437
+ onChange={handleTargetFormChange('webhookUrl')}
438
+ />
439
+ <Field.Error />
440
+ </Field.Root>
441
+ </Grid.Item>
442
+ )}
387
443
  </Grid.Root>
388
444
  <Flex gap={2} justifyContent="flex-end">
389
445
  <Button variant="tertiary" onClick={resetTargetForm}>
@@ -405,41 +461,60 @@ const SettingsPage = () => {
405
461
  <Table>
406
462
  <Thead>
407
463
  <Tr>
464
+ <Th><Typography variant="sigma">Type</Typography></Th>
408
465
  <Th><Typography variant="sigma">Name</Typography></Th>
409
- <Th><Typography variant="sigma">Workflow</Typography></Th>
410
- <Th><Typography variant="sigma">Branch</Typography></Th>
466
+ <Th><Typography variant="sigma">Details</Typography></Th>
411
467
  <Th><Typography variant="sigma">Actions</Typography></Th>
412
468
  </Tr>
413
469
  </Thead>
414
470
  <Tbody>
415
- {settings.targets.map((target) => (
416
- <Tr key={target.id}>
417
- <Td><Typography variant="omega">{target.name}</Typography></Td>
418
- <Td><Typography variant="omega">{target.workflow}</Typography></Td>
419
- <Td><Typography variant="omega">{target.branch}</Typography></Td>
420
- <Td>
421
- <Flex gap={1}>
422
- <IconButton
423
- onClick={() => handleEditTarget(target)}
424
- label="Edit"
425
- variant="ghost"
426
- >
427
- <Pencil />
428
- </IconButton>
429
- <IconButton
430
- onClick={() => {
431
- setTargetToDelete(target.id);
432
- setDeleteDialogOpen(true);
433
- }}
434
- label="Delete"
435
- variant="ghost"
436
- >
437
- <Trash />
438
- </IconButton>
439
- </Flex>
440
- </Td>
441
- </Tr>
442
- ))}
471
+ {settings.targets.map((target) => {
472
+ const targetType = target.type || 'github';
473
+ return (
474
+ <Tr key={target.id}>
475
+ <Td>
476
+ <Typography variant="omega" fontWeight="bold" textColor={targetType === 'github' ? 'neutral800' : 'secondary600'}>
477
+ {targetType === 'github' ? 'GitHub' : 'Vercel'}
478
+ </Typography>
479
+ </Td>
480
+ <Td><Typography variant="omega">{target.name}</Typography></Td>
481
+ <Td>
482
+ {targetType === 'github' ? (
483
+ <Typography variant="omega" textColor="neutral600">
484
+ {target.workflow} / {target.branch}
485
+ </Typography>
486
+ ) : (
487
+ <Typography variant="omega" textColor="neutral600">
488
+ Webhook configured
489
+ </Typography>
490
+ )}
491
+ </Td>
492
+ <Td>
493
+ <Flex gap={2}>
494
+ <Button
495
+ onClick={() => handleEditTarget(target)}
496
+ variant="tertiary"
497
+ size="S"
498
+ startIcon={<Pencil />}
499
+ >
500
+ Edit
501
+ </Button>
502
+ <Button
503
+ onClick={() => {
504
+ setTargetToDelete(target.id);
505
+ setDeleteDialogOpen(true);
506
+ }}
507
+ variant="danger-light"
508
+ size="S"
509
+ startIcon={<Trash />}
510
+ >
511
+ Delete
512
+ </Button>
513
+ </Flex>
514
+ </Td>
515
+ </Tr>
516
+ );
517
+ })}
443
518
  </Tbody>
444
519
  </Table>
445
520
  ) : (