@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.
Files changed (38) hide show
  1. package/README.md +116 -0
  2. package/admin/jsconfig.json +10 -0
  3. package/admin/src/components/Initializer.jsx +18 -0
  4. package/admin/src/components/PluginIcon.jsx +5 -0
  5. package/admin/src/index.js +49 -0
  6. package/admin/src/pages/App.jsx +17 -0
  7. package/admin/src/pages/HomePage.jsx +241 -0
  8. package/admin/src/pages/SettingsPage.jsx +370 -0
  9. package/admin/src/pluginId.js +1 -0
  10. package/admin/src/translations/en.json +3 -0
  11. package/admin/src/utils/getTranslation.js +5 -0
  12. package/dist/_chunks/App-3JntxPYv.js +520 -0
  13. package/dist/_chunks/App-C0Byi5W1.mjs +520 -0
  14. package/dist/_chunks/en-BDvOU5UD.js +6 -0
  15. package/dist/_chunks/en-DdBZuj6F.mjs +6 -0
  16. package/dist/_chunks/index-C18aSW5z.mjs +70 -0
  17. package/dist/_chunks/index-CqpMwL_C.js +69 -0
  18. package/dist/admin/index.js +3 -0
  19. package/dist/admin/index.mjs +4 -0
  20. package/dist/server/index.js +264 -0
  21. package/dist/server/index.mjs +265 -0
  22. package/package.json +84 -0
  23. package/server/jsconfig.json +10 -0
  24. package/server/src/bootstrap.js +5 -0
  25. package/server/src/config/index.js +4 -0
  26. package/server/src/content-types/index.js +1 -0
  27. package/server/src/controllers/controller.js +95 -0
  28. package/server/src/controllers/index.js +5 -0
  29. package/server/src/destroy.js +5 -0
  30. package/server/src/index.js +31 -0
  31. package/server/src/middlewares/index.js +1 -0
  32. package/server/src/policies/index.js +1 -0
  33. package/server/src/register.js +5 -0
  34. package/server/src/routes/admin/index.js +37 -0
  35. package/server/src/routes/content-api/index.js +14 -0
  36. package/server/src/routes/index.js +9 -0
  37. package/server/src/services/index.js +5 -0
  38. 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';
@@ -0,0 +1,3 @@
1
+ {
2
+ "plugin.name": "Deployment Trigger"
3
+ }
@@ -0,0 +1,5 @@
1
+ import { PLUGIN_ID } from '../pluginId';
2
+
3
+ const getTranslation = (id) => `${PLUGIN_ID}.${id}`;
4
+
5
+ export { getTranslation };