@orange-soft/strapi-deployment-trigger 1.0.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 +116 -0
- package/admin/jsconfig.json +10 -0
- package/admin/src/components/Initializer.jsx +18 -0
- package/admin/src/components/PluginIcon.jsx +5 -0
- package/admin/src/index.js +49 -0
- package/admin/src/pages/App.jsx +17 -0
- package/admin/src/pages/HomePage.jsx +241 -0
- package/admin/src/pages/SettingsPage.jsx +370 -0
- package/admin/src/pluginId.js +1 -0
- package/admin/src/translations/en.json +3 -0
- package/admin/src/utils/getTranslation.js +5 -0
- package/dist/_chunks/App-3JntxPYv.js +520 -0
- package/dist/_chunks/App-C0Byi5W1.mjs +520 -0
- package/dist/_chunks/en-BDvOU5UD.js +6 -0
- package/dist/_chunks/en-DdBZuj6F.mjs +6 -0
- package/dist/_chunks/index-C18aSW5z.mjs +70 -0
- package/dist/_chunks/index-CqpMwL_C.js +69 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/server/index.js +264 -0
- package/dist/server/index.mjs +265 -0
- package/package.json +84 -0
- package/server/jsconfig.json +10 -0
- package/server/src/bootstrap.js +5 -0
- package/server/src/config/index.js +4 -0
- package/server/src/content-types/index.js +1 -0
- package/server/src/controllers/controller.js +95 -0
- package/server/src/controllers/index.js +5 -0
- package/server/src/destroy.js +5 -0
- package/server/src/index.js +31 -0
- package/server/src/middlewares/index.js +1 -0
- package/server/src/policies/index.js +1 -0
- package/server/src/register.js +5 -0
- package/server/src/routes/admin/index.js +37 -0
- package/server/src/routes/content-api/index.js +14 -0
- package/server/src/routes/index.js +9 -0
- package/server/src/services/index.js +5 -0
- package/server/src/services/service.js +124 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useFetchClient, Layouts, BackButton } from '@strapi/strapi/admin';
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
Button,
|
|
6
|
+
Flex,
|
|
7
|
+
Typography,
|
|
8
|
+
Alert,
|
|
9
|
+
Loader,
|
|
10
|
+
Field,
|
|
11
|
+
Grid,
|
|
12
|
+
} from '@strapi/design-system';
|
|
13
|
+
import { Check } from '@strapi/icons';
|
|
14
|
+
|
|
15
|
+
import { PLUGIN_ID } from '../pluginId';
|
|
16
|
+
|
|
17
|
+
// Validation patterns
|
|
18
|
+
const TOKEN_PATTERN = /^github_pat_[a-zA-Z0-9_]+$/;
|
|
19
|
+
const REPO_URL_PATTERN = /^https:\/\/github\.com\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+\/?$/;
|
|
20
|
+
const WORKFLOW_PATTERN = /^[a-zA-Z0-9_.-]+\.ya?ml$/;
|
|
21
|
+
|
|
22
|
+
const validateToken = (value) => {
|
|
23
|
+
if (!value) return null; // Optional if already configured
|
|
24
|
+
if (!TOKEN_PATTERN.test(value)) {
|
|
25
|
+
return 'Token must start with "github_pat_" followed by alphanumeric characters';
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const validateRepoUrl = (value) => {
|
|
31
|
+
if (!value) return 'Repository URL is required';
|
|
32
|
+
if (!REPO_URL_PATTERN.test(value)) {
|
|
33
|
+
return 'Must be a valid GitHub URL (e.g., https://github.com/owner/repo)';
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const validateWorkflow = (value) => {
|
|
39
|
+
if (!value) return null; // Optional, has default
|
|
40
|
+
if (!WORKFLOW_PATTERN.test(value)) {
|
|
41
|
+
return 'Workflow file must end with .yml or .yaml';
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const SettingsPage = () => {
|
|
47
|
+
const { get, put } = useFetchClient();
|
|
48
|
+
const [settings, setSettings] = useState({
|
|
49
|
+
githubToken: '',
|
|
50
|
+
repoUrl: '',
|
|
51
|
+
workflow: '',
|
|
52
|
+
branch: '',
|
|
53
|
+
});
|
|
54
|
+
const [errors, setErrors] = useState({});
|
|
55
|
+
const [hasExistingToken, setHasExistingToken] = useState(false);
|
|
56
|
+
const [maskedToken, setMaskedToken] = useState(null);
|
|
57
|
+
const [loading, setLoading] = useState(true);
|
|
58
|
+
const [saving, setSaving] = useState(false);
|
|
59
|
+
const [notification, setNotification] = useState(null);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
fetchSettings();
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const fetchSettings = async () => {
|
|
66
|
+
setLoading(true);
|
|
67
|
+
try {
|
|
68
|
+
const { data } = await get(`/${PLUGIN_ID}/settings`);
|
|
69
|
+
const fetchedSettings = data.data || {};
|
|
70
|
+
setSettings({
|
|
71
|
+
githubToken: '',
|
|
72
|
+
repoUrl: fetchedSettings.repoUrl || '',
|
|
73
|
+
workflow: fetchedSettings.workflow || '',
|
|
74
|
+
branch: fetchedSettings.branch || '',
|
|
75
|
+
});
|
|
76
|
+
setHasExistingToken(fetchedSettings.hasToken || false);
|
|
77
|
+
setMaskedToken(fetchedSettings.maskedToken || null);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Error fetching settings:', error);
|
|
80
|
+
setNotification({
|
|
81
|
+
type: 'danger',
|
|
82
|
+
message: 'Failed to load settings',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
setLoading(false);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const validateAll = () => {
|
|
89
|
+
const newErrors = {};
|
|
90
|
+
|
|
91
|
+
const tokenError = validateToken(settings.githubToken);
|
|
92
|
+
if (tokenError && settings.githubToken) newErrors.githubToken = tokenError;
|
|
93
|
+
|
|
94
|
+
const repoError = validateRepoUrl(settings.repoUrl);
|
|
95
|
+
if (repoError) newErrors.repoUrl = repoError;
|
|
96
|
+
|
|
97
|
+
const workflowError = validateWorkflow(settings.workflow);
|
|
98
|
+
if (workflowError) newErrors.workflow = workflowError;
|
|
99
|
+
|
|
100
|
+
setErrors(newErrors);
|
|
101
|
+
return Object.keys(newErrors).length === 0;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleSave = async () => {
|
|
105
|
+
if (!validateAll()) {
|
|
106
|
+
setNotification({
|
|
107
|
+
type: 'warning',
|
|
108
|
+
message: 'Please fix the validation errors before saving',
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setSaving(true);
|
|
114
|
+
try {
|
|
115
|
+
const dataToSave = { ...settings };
|
|
116
|
+
if (!dataToSave.githubToken) {
|
|
117
|
+
delete dataToSave.githubToken;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await put(`/${PLUGIN_ID}/settings`, { data: dataToSave });
|
|
121
|
+
|
|
122
|
+
if (settings.githubToken) {
|
|
123
|
+
setHasExistingToken(true);
|
|
124
|
+
}
|
|
125
|
+
setSettings(prev => ({ ...prev, githubToken: '' }));
|
|
126
|
+
|
|
127
|
+
setNotification({
|
|
128
|
+
type: 'success',
|
|
129
|
+
message: 'Settings saved successfully',
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('Error saving settings:', error);
|
|
133
|
+
setNotification({
|
|
134
|
+
type: 'danger',
|
|
135
|
+
message: 'Failed to save settings',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
setSaving(false);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handleChange = (field) => (e) => {
|
|
142
|
+
const value = e.target.value;
|
|
143
|
+
setSettings(prev => ({ ...prev, [field]: value }));
|
|
144
|
+
|
|
145
|
+
// Clear error when user starts typing
|
|
146
|
+
if (errors[field]) {
|
|
147
|
+
setErrors(prev => ({ ...prev, [field]: null }));
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleBlur = (field, validator) => () => {
|
|
152
|
+
const error = validator(settings[field]);
|
|
153
|
+
if (error) {
|
|
154
|
+
setErrors(prev => ({ ...prev, [field]: error }));
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (loading) {
|
|
159
|
+
return (
|
|
160
|
+
<Layouts.Root>
|
|
161
|
+
<Layouts.Header
|
|
162
|
+
title="Settings"
|
|
163
|
+
subtitle="Configure GitHub Actions deployment"
|
|
164
|
+
navigationAction={<BackButton fallback={`/plugins/${PLUGIN_ID}`} />}
|
|
165
|
+
/>
|
|
166
|
+
<Layouts.Content>
|
|
167
|
+
<Flex justifyContent="center" padding={8}>
|
|
168
|
+
<Loader>Loading settings...</Loader>
|
|
169
|
+
</Flex>
|
|
170
|
+
</Layouts.Content>
|
|
171
|
+
</Layouts.Root>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const isValid = settings.repoUrl && !errors.repoUrl && !errors.githubToken && !errors.workflow;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<Layouts.Root>
|
|
179
|
+
<Layouts.Header
|
|
180
|
+
title="Settings"
|
|
181
|
+
subtitle="Configure GitHub Actions deployment"
|
|
182
|
+
navigationAction={<BackButton fallback={`/plugins/${PLUGIN_ID}`} />}
|
|
183
|
+
primaryAction={
|
|
184
|
+
<Button
|
|
185
|
+
onClick={handleSave}
|
|
186
|
+
loading={saving}
|
|
187
|
+
disabled={loading || !isValid}
|
|
188
|
+
startIcon={<Check />}
|
|
189
|
+
size="L"
|
|
190
|
+
>
|
|
191
|
+
Save Settings
|
|
192
|
+
</Button>
|
|
193
|
+
}
|
|
194
|
+
/>
|
|
195
|
+
<Layouts.Content>
|
|
196
|
+
{notification && (
|
|
197
|
+
<Box paddingBottom={4}>
|
|
198
|
+
<Alert
|
|
199
|
+
closeLabel="Close"
|
|
200
|
+
title={notification.message}
|
|
201
|
+
variant={notification.type}
|
|
202
|
+
onClose={() => setNotification(null)}
|
|
203
|
+
/>
|
|
204
|
+
</Box>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
<Flex direction="column" alignItems="stretch" gap={6}>
|
|
208
|
+
{/* Repository Section */}
|
|
209
|
+
<Box
|
|
210
|
+
background="neutral0"
|
|
211
|
+
hasRadius
|
|
212
|
+
shadow="filterShadow"
|
|
213
|
+
paddingTop={6}
|
|
214
|
+
paddingBottom={6}
|
|
215
|
+
paddingLeft={7}
|
|
216
|
+
paddingRight={7}
|
|
217
|
+
>
|
|
218
|
+
<Flex direction="column" alignItems="stretch" gap={4}>
|
|
219
|
+
<Typography variant="delta" tag="h2">
|
|
220
|
+
Repository
|
|
221
|
+
</Typography>
|
|
222
|
+
|
|
223
|
+
<Field.Root
|
|
224
|
+
name="repoUrl"
|
|
225
|
+
required
|
|
226
|
+
error={errors.repoUrl}
|
|
227
|
+
hint="Copy the URL from your browser when viewing the repository"
|
|
228
|
+
>
|
|
229
|
+
<Field.Label>Repository URL</Field.Label>
|
|
230
|
+
<Field.Input
|
|
231
|
+
placeholder="https://github.com/{owner}/{repo}"
|
|
232
|
+
value={settings.repoUrl}
|
|
233
|
+
onChange={handleChange('repoUrl')}
|
|
234
|
+
onBlur={handleBlur('repoUrl', validateRepoUrl)}
|
|
235
|
+
/>
|
|
236
|
+
<Field.Hint />
|
|
237
|
+
<Field.Error />
|
|
238
|
+
</Field.Root>
|
|
239
|
+
</Flex>
|
|
240
|
+
</Box>
|
|
241
|
+
|
|
242
|
+
{/* Workflow Section */}
|
|
243
|
+
<Box
|
|
244
|
+
background="neutral0"
|
|
245
|
+
hasRadius
|
|
246
|
+
shadow="filterShadow"
|
|
247
|
+
paddingTop={6}
|
|
248
|
+
paddingBottom={6}
|
|
249
|
+
paddingLeft={7}
|
|
250
|
+
paddingRight={7}
|
|
251
|
+
>
|
|
252
|
+
<Flex direction="column" alignItems="stretch" gap={4}>
|
|
253
|
+
<Typography variant="delta" tag="h2">
|
|
254
|
+
Workflow Configuration
|
|
255
|
+
</Typography>
|
|
256
|
+
|
|
257
|
+
<Grid.Root gap={4}>
|
|
258
|
+
<Grid.Item col={6} s={12}>
|
|
259
|
+
<Field.Root
|
|
260
|
+
name="workflow"
|
|
261
|
+
required
|
|
262
|
+
error={errors.workflow}
|
|
263
|
+
hint="Filename in .github/workflows/"
|
|
264
|
+
>
|
|
265
|
+
<Field.Label>Workflow File</Field.Label>
|
|
266
|
+
<Field.Input
|
|
267
|
+
placeholder="deploy.yml"
|
|
268
|
+
value={settings.workflow}
|
|
269
|
+
onChange={handleChange('workflow')}
|
|
270
|
+
onBlur={handleBlur('workflow', validateWorkflow)}
|
|
271
|
+
/>
|
|
272
|
+
<Field.Hint />
|
|
273
|
+
<Field.Error />
|
|
274
|
+
</Field.Root>
|
|
275
|
+
</Grid.Item>
|
|
276
|
+
|
|
277
|
+
<Grid.Item col={6} s={12}>
|
|
278
|
+
<Field.Root
|
|
279
|
+
name="branch"
|
|
280
|
+
required
|
|
281
|
+
hint="Branch to trigger the workflow on"
|
|
282
|
+
>
|
|
283
|
+
<Field.Label>Branch</Field.Label>
|
|
284
|
+
<Field.Input
|
|
285
|
+
placeholder="main"
|
|
286
|
+
value={settings.branch}
|
|
287
|
+
onChange={handleChange('branch')}
|
|
288
|
+
/>
|
|
289
|
+
<Field.Hint />
|
|
290
|
+
</Field.Root>
|
|
291
|
+
</Grid.Item>
|
|
292
|
+
</Grid.Root>
|
|
293
|
+
</Flex>
|
|
294
|
+
</Box>
|
|
295
|
+
|
|
296
|
+
{/* Authentication Section */}
|
|
297
|
+
<Box
|
|
298
|
+
background="neutral0"
|
|
299
|
+
hasRadius
|
|
300
|
+
shadow="filterShadow"
|
|
301
|
+
paddingTop={6}
|
|
302
|
+
paddingBottom={6}
|
|
303
|
+
paddingLeft={7}
|
|
304
|
+
paddingRight={7}
|
|
305
|
+
>
|
|
306
|
+
<Flex direction="column" alignItems="stretch" gap={4}>
|
|
307
|
+
<Typography variant="delta" tag="h2">
|
|
308
|
+
Authentication
|
|
309
|
+
</Typography>
|
|
310
|
+
|
|
311
|
+
<Field.Root
|
|
312
|
+
name="githubToken"
|
|
313
|
+
required
|
|
314
|
+
error={errors.githubToken}
|
|
315
|
+
hint={
|
|
316
|
+
!hasExistingToken || !maskedToken
|
|
317
|
+
? "Create a fine-grained token with Actions (Read and write) permission"
|
|
318
|
+
: undefined
|
|
319
|
+
}
|
|
320
|
+
>
|
|
321
|
+
<Field.Label>GitHub Personal Access Token</Field.Label>
|
|
322
|
+
<Field.Input
|
|
323
|
+
type="password"
|
|
324
|
+
placeholder={hasExistingToken ? "••••••••••••••••" : "github_pat_xxxxxxxxxxxx"}
|
|
325
|
+
value={settings.githubToken}
|
|
326
|
+
onChange={handleChange('githubToken')}
|
|
327
|
+
onBlur={handleBlur('githubToken', validateToken)}
|
|
328
|
+
/>
|
|
329
|
+
{hasExistingToken && maskedToken ? (
|
|
330
|
+
<Typography variant="pi" textColor="neutral600">
|
|
331
|
+
Existing token:{' '}
|
|
332
|
+
<Typography
|
|
333
|
+
variant="pi"
|
|
334
|
+
fontWeight="bold"
|
|
335
|
+
textColor="success600"
|
|
336
|
+
tag="span"
|
|
337
|
+
>
|
|
338
|
+
{maskedToken}
|
|
339
|
+
</Typography>
|
|
340
|
+
. Leave empty to keep existing, or enter new to replace.
|
|
341
|
+
</Typography>
|
|
342
|
+
) : (
|
|
343
|
+
<Field.Hint />
|
|
344
|
+
)}
|
|
345
|
+
<Field.Error />
|
|
346
|
+
</Field.Root>
|
|
347
|
+
|
|
348
|
+
<Box paddingTop={2}>
|
|
349
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
350
|
+
How to get a GitHub Token:
|
|
351
|
+
</Typography>
|
|
352
|
+
<Box paddingTop={2}>
|
|
353
|
+
<Typography variant="pi" tag="ol" textColor="neutral600">
|
|
354
|
+
<li>Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens</li>
|
|
355
|
+
<li>Click "Generate new token"</li>
|
|
356
|
+
<li>Set token name, expiration, and select the target repository</li>
|
|
357
|
+
<li>Under "Repository permissions", set <strong>Actions</strong> to "Read and write"</li>
|
|
358
|
+
<li>Click "Generate token" and paste it above</li>
|
|
359
|
+
</Typography>
|
|
360
|
+
</Box>
|
|
361
|
+
</Box>
|
|
362
|
+
</Flex>
|
|
363
|
+
</Box>
|
|
364
|
+
</Flex>
|
|
365
|
+
</Layouts.Content>
|
|
366
|
+
</Layouts.Root>
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export { SettingsPage };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PLUGIN_ID = 'deployment-trigger';
|