@underpostnet/underpost 3.0.3 → 3.1.1

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 (82) hide show
  1. package/{.env.production → .env.example} +20 -2
  2. package/.github/workflows/ghpkg.ci.yml +1 -1
  3. package/.github/workflows/gitlab.ci.yml +1 -1
  4. package/.github/workflows/npmpkg.ci.yml +22 -7
  5. package/.github/workflows/publish.ci.yml +5 -5
  6. package/.github/workflows/pwa-microservices-template-page.cd.yml +3 -3
  7. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  8. package/.github/workflows/release.cd.yml +3 -2
  9. package/.vscode/extensions.json +9 -8
  10. package/.vscode/settings.json +3 -2
  11. package/CHANGELOG.md +160 -1
  12. package/CLI-HELP.md +71 -52
  13. package/README.md +2 -2
  14. package/bin/build.js +4 -1
  15. package/bin/deploy.js +150 -208
  16. package/bin/file.js +2 -1
  17. package/bin/vs.js +3 -3
  18. package/conf.js +30 -13
  19. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  20. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  21. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  22. package/manifests/deployment/dd-test-development/deployment.yaml +52 -52
  23. package/manifests/deployment/dd-test-development/proxy.yaml +4 -4
  24. package/manifests/pv-pvc-dd.yaml +1 -1
  25. package/package.json +48 -43
  26. package/scripts/k3s-node-setup.sh +1 -1
  27. package/src/api/document/document.service.js +1 -1
  28. package/src/api/file/file.controller.js +3 -1
  29. package/src/api/file/file.service.js +28 -5
  30. package/src/api/user/user.router.js +10 -5
  31. package/src/api/user/user.service.js +7 -7
  32. package/src/cli/baremetal.js +6 -10
  33. package/src/cli/cloud-init.js +0 -3
  34. package/src/cli/db.js +54 -71
  35. package/src/cli/deploy.js +64 -12
  36. package/src/cli/env.js +5 -5
  37. package/src/cli/fs.js +0 -2
  38. package/src/cli/image.js +0 -3
  39. package/src/cli/index.js +27 -13
  40. package/src/cli/monitor.js +5 -6
  41. package/src/cli/repository.js +329 -35
  42. package/src/cli/run.js +118 -69
  43. package/src/cli/secrets.js +1 -3
  44. package/src/cli/ssh.js +1 -1
  45. package/src/client/components/core/AgGrid.js +20 -5
  46. package/src/client/components/core/Content.js +22 -3
  47. package/src/client/components/core/Docs.js +21 -4
  48. package/src/client/components/core/FileExplorer.js +71 -4
  49. package/src/client/components/core/Input.js +1 -1
  50. package/src/client/components/core/Modal.js +20 -6
  51. package/src/client/public/default/sitemap +3 -3
  52. package/src/client/public/test/sitemap +3 -3
  53. package/src/client.build.js +0 -3
  54. package/src/client.dev.js +0 -3
  55. package/src/db/DataBaseProvider.js +17 -2
  56. package/src/db/mariadb/MariaDB.js +14 -9
  57. package/src/db/mongo/MongooseDB.js +17 -1
  58. package/src/index.js +1 -1
  59. package/src/proxy.js +0 -3
  60. package/src/runtime/express/Express.js +7 -1
  61. package/src/runtime/lampp/Lampp.js +6 -13
  62. package/src/server/auth.js +6 -9
  63. package/src/server/backup.js +2 -3
  64. package/src/server/client-build-docs.js +178 -3
  65. package/src/server/client-build-live.js +9 -18
  66. package/src/server/client-build.js +175 -38
  67. package/src/server/client-dev-server.js +14 -13
  68. package/src/server/conf.js +357 -149
  69. package/src/server/cron.js +2 -1
  70. package/src/server/dns.js +28 -12
  71. package/src/server/downloader.js +0 -2
  72. package/src/server/logger.js +27 -9
  73. package/src/server/peer.js +0 -2
  74. package/src/server/process.js +1 -50
  75. package/src/server/proxy.js +4 -8
  76. package/src/server/runtime.js +5 -8
  77. package/src/server/ssr.js +0 -3
  78. package/src/server/start.js +5 -5
  79. package/src/server/tls.js +0 -2
  80. package/src/server.js +0 -4
  81. package/.env.development +0 -43
  82. package/.env.test +0 -43
@@ -4,18 +4,27 @@
4
4
  * @namespace UnderpostRepository
5
5
  */
6
6
 
7
- import { commitData } from '../client/components/core/CommonJs.js';
8
7
  import dotenv from 'dotenv';
8
+ import { commitData } from '../client/components/core/CommonJs.js';
9
9
  import { pbcopy, shellCd, shellExec } from '../server/process.js';
10
10
  import { actionInitLog, loggerFactory } from '../server/logger.js';
11
11
  import fs from 'fs-extra';
12
- import { getNpmRootPath } from '../server/conf.js';
13
- import { Config } from '../server/conf.js';
12
+ import {
13
+ getNpmRootPath,
14
+ Config,
15
+ loadConf,
16
+ readConfJson,
17
+ getConfFilePath,
18
+ loadReplicas,
19
+ loadConfServerJson,
20
+ getDataDeploy,
21
+ buildReplicaId,
22
+ writeEnv,
23
+ } from '../server/conf.js';
24
+ import { buildClient } from '../server/client-build.js';
14
25
  import { DefaultConf } from '../../conf.js';
15
26
  import Underpost from '../index.js';
16
27
 
17
- dotenv.config();
18
-
19
28
  const logger = loggerFactory(import.meta);
20
29
 
21
30
  const diffCmd = `--no-pager show -U0 -w --word-diff=color --word-diff-regex='[^[:space:]]' --color=always`;
@@ -37,8 +46,8 @@ class UnderpostRepository {
37
46
  * @param {boolean} [options.g8=false] - If true, uses the .g8 extension.
38
47
  * @memberof UnderpostRepository
39
48
  */
40
- clone(gitUri = `${process.env.GITHUB_USERNAME}/pwa-microservices-template`, options = { bare: false, G8: false }) {
41
- const gExtension = options.G8 === true ? '.g8' : '.git';
49
+ clone(gitUri = `${process.env.GITHUB_USERNAME}/pwa-microservices-template`, options = { bare: false, g8: false }) {
50
+ const gExtension = options.g8 === true ? '.g8' : '.git';
42
51
  const repoName = gitUri.split('/').pop();
43
52
  if (fs.existsSync(`./${repoName}`)) fs.removeSync(`./${repoName}`);
44
53
  shellExec(
@@ -54,16 +63,16 @@ class UnderpostRepository {
54
63
  * Pulls updates from a GitHub repository.
55
64
  * @param {string} [repoPath='./'] - The local path to the repository.
56
65
  * @param {string} [gitUri=`${process.env.GITHUB_USERNAME}/pwa-microservices-template`] - The URI of the GitHub repository.
57
- * @param {object} [options={ G8: false }] - Pulling options.
66
+ * @param {object} [options={ g8: false }] - Pulling options.
58
67
  * @param {boolean} [options.g8=false] - If true, uses the .g8 extension.
59
68
  * @memberof UnderpostRepository
60
69
  */
61
70
  pull(
62
71
  repoPath = './',
63
72
  gitUri = `${process.env.GITHUB_USERNAME}/pwa-microservices-template`,
64
- options = { G8: false },
73
+ options = { g8: false },
65
74
  ) {
66
- const gExtension = options.G8 === true ? '.g8' : '.git';
75
+ const gExtension = options.g8 === true ? '.g8' : '.git';
67
76
  shellExec(
68
77
  `cd ${repoPath} && git pull https://${
69
78
  process.env.GITHUB_TOKEN ? `${process.env.GITHUB_TOKEN}@` : ''
@@ -118,15 +127,26 @@ class UnderpostRepository {
118
127
  changelogBuild: false,
119
128
  changelogMinVersion: '',
120
129
  changelogNoHash: false,
130
+ b: false,
121
131
  },
122
132
  ) {
123
133
  if (!repoPath) repoPath = '.';
124
134
 
135
+ if (options.b) {
136
+ const currentBranch = shellExec(`cd ${repoPath} && git branch --show-current`, {
137
+ stdout: true,
138
+ silent: true,
139
+ disableLog: true,
140
+ }).trim();
141
+ if (options.copy) pbcopy(currentBranch);
142
+ else console.log(currentBranch);
143
+ return;
144
+ }
145
+
125
146
  if (options.changelog !== undefined || options.changelogBuild) {
126
147
  const ciIntegrationPrefix = 'ci(package-pwa-microservices-';
127
148
  const ciIntegrationVersionPrefix = 'New release v:';
128
149
  const releaseMatch = 'New release v:';
129
-
130
150
  // Helper: parse [<tag>] commits into grouped sections
131
151
  const buildSectionChangelog = (commits) => {
132
152
  const groups = {};
@@ -261,11 +281,26 @@ class UnderpostRepository {
261
281
 
262
282
  let commits;
263
283
  if (!hasExplicitCount) {
264
- // No explicit count: find commits up to the last CI integration boundary
265
- const ciIndex = allCommits.findIndex(
266
- (c) => c.message.startsWith(ciIntegrationPrefix) && !c.message.match(ciIntegrationVersionPrefix),
267
- );
268
- commits = ciIndex >= 0 ? allCommits.slice(0, ciIndex) : allCommits;
284
+ let boundaryIndex = -1;
285
+
286
+ // New boundary: deploy hash stored in config via `underpost config set LAST_CI_DEPLOY_HASH`
287
+ const lastDeployHash = shellExec('underpost config get LAST_CI_DEPLOY_HASH --plain', {
288
+ stdout: true,
289
+ silent: true,
290
+ disableLog: true,
291
+ }).trim();
292
+ if (lastDeployHash && lastDeployHash !== 'undefined' && lastDeployHash !== 'null') {
293
+ boundaryIndex = allCommits.findIndex((c) => c.fullHash === lastDeployHash || c.hash === lastDeployHash);
294
+ }
295
+
296
+ // Fallback: old CI integration commit boundary
297
+ if (boundaryIndex < 0) {
298
+ boundaryIndex = allCommits.findIndex(
299
+ (c) => c.message.startsWith(ciIntegrationPrefix) && !c.message.match(ciIntegrationVersionPrefix),
300
+ );
301
+ }
302
+
303
+ commits = boundaryIndex >= 0 ? allCommits.slice(0, boundaryIndex) : allCommits;
269
304
  } else {
270
305
  commits = allCommits;
271
306
  }
@@ -360,7 +395,7 @@ class UnderpostRepository {
360
395
  * Pushes commits to a remote GitHub repository.
361
396
  * @param {string} [repoPath='./'] - The local path to the repository.
362
397
  * @param {string} [gitUri=`${process.env.GITHUB_USERNAME}/pwa-microservices-template`] - The URI of the GitHub repository.
363
- * @param {object} [options={ f: false, G8: false }] - Push options.
398
+ * @param {object} [options={ f: false, g8: false }] - Push options.
364
399
  * @param {boolean} [options.f=false] - If true, forces the push.
365
400
  * @param {boolean} [options.g8=false] - If true, uses the .g8 extension.
366
401
  * @memberof UnderpostRepository
@@ -368,9 +403,9 @@ class UnderpostRepository {
368
403
  push(
369
404
  repoPath = './',
370
405
  gitUri = `${process.env.GITHUB_USERNAME}/pwa-microservices-template`,
371
- options = { f: false, G8: false },
406
+ options = { f: false, g8: false },
372
407
  ) {
373
- const gExtension = options.G8 === true ? '.g8' : '.git';
408
+ const gExtension = options.g8 === true ? '.g8' : '.git';
374
409
  shellExec(
375
410
  `cd ${repoPath} && git push https://${process.env.GITHUB_TOKEN}@github.com/${gitUri}${gExtension}${
376
411
  options?.f === true ? ' --force' : ''
@@ -403,6 +438,7 @@ class UnderpostRepository {
403
438
  * @param {boolean} [options.cleanTemplate=false] - If true, cleans the pwa-microservices-template build directory.
404
439
  * @param {boolean} [options.build=false] - If true, builds the deployment to pwa-microservices-template (requires deployId).
405
440
  * @param {boolean} [options.syncConf=false] - If true, syncs configuration to private repositories (requires deployId).
441
+ * @param {boolean} [options.syncStart=false] - If true, syncs start scripts in deploy ID package.json with root package.json.
406
442
  * @param {boolean} [options.defaultConf=false] - If true, updates the default configuration file (requires deployId).
407
443
  * @param {string} [options.confWorkflowId=''] - If provided, uses this configuration workflow ID.
408
444
  * @returns {Promise<boolean>} A promise that resolves when the initialization is complete.
@@ -420,6 +456,7 @@ class UnderpostRepository {
420
456
  cleanTemplate: false,
421
457
  build: false,
422
458
  syncConf: false,
459
+ syncStart: false,
423
460
  defaultConf: false,
424
461
  confWorkflowId: '',
425
462
  },
@@ -448,6 +485,13 @@ class UnderpostRepository {
448
485
 
449
486
  if (options.deployId) {
450
487
  let deployId = options.deployId;
488
+
489
+ // Handle sync-start operation (before dd- prefix normalization to support 'dd' special case)
490
+ if (options.syncStart) {
491
+ shellExec(`node bin/deploy sync-start ${deployId}`);
492
+ return resolve(true);
493
+ }
494
+
451
495
  if (!deployId.startsWith('dd-')) deployId = `dd-${deployId}`;
452
496
  // Handle purge operation
453
497
  if (options.purge) {
@@ -547,7 +591,6 @@ class UnderpostRepository {
547
591
  const npmRoot = getNpmRootPath();
548
592
  const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
549
593
  const destFolder = `./${projectName}`;
550
- logger.info('Note: This process may take several minutes to complete');
551
594
  logger.info('build app', { destFolder });
552
595
  if (fs.existsSync(destFolder)) fs.removeSync(destFolder);
553
596
  fs.mkdirSync(destFolder, { recursive: true });
@@ -572,6 +615,215 @@ class UnderpostRepository {
572
615
  });
573
616
  },
574
617
 
618
+ /**
619
+ * Builds client assets, single replicas, and/or syncs environment ports.
620
+ * @param {string} [deployId='dd-default'] - The deployment ID.
621
+ * @param {string} [subConf=''] - The sub-configuration for the build.
622
+ * @param {string} [host=''] - Comma-separated hosts to filter the build.
623
+ * @param {string} [path=''] - Comma-separated paths to filter the build.
624
+ * @param {object} [options] - Build options.
625
+ * @param {boolean} [options.syncEnvPort=false] - If true, syncs environment port assignments across all deploy IDs.
626
+ * @param {boolean} [options.singleReplica=false] - If true, builds single replica folders instead of full client.
627
+ * @returns {Promise<boolean>} A promise that resolves when the build is complete.
628
+ * @memberof UnderpostRepository
629
+ */
630
+ client(
631
+ deployId = 'dd-default',
632
+ subConf = '',
633
+ host = '',
634
+ path = '',
635
+ options = {
636
+ syncEnvPort: false,
637
+ singleReplica: false,
638
+ },
639
+ ) {
640
+ return new Promise(async (resolve, reject) => {
641
+ try {
642
+ // Handle syncEnvPort operation
643
+ if (options.syncEnvPort) {
644
+ const dataDeploy = await getDataDeploy({ disableSyncEnvPort: true });
645
+ const dataEnv = [
646
+ { env: 'production', port: 3000 },
647
+ { env: 'development', port: 4000 },
648
+ { env: 'test', port: 5000 },
649
+ ];
650
+ let portOffset = 0;
651
+ const singleReplicaPortOffsets = {};
652
+ for (const deployIdObj of dataDeploy) {
653
+ const { deployId } = deployIdObj;
654
+ const baseConfPath = fs.existsSync(`./engine-private/replica/${deployId}`)
655
+ ? `./engine-private/replica`
656
+ : `./engine-private/conf`;
657
+
658
+ const effectivePortOffset =
659
+ singleReplicaPortOffsets[deployId] !== undefined ? singleReplicaPortOffsets[deployId] : portOffset;
660
+
661
+ for (const envInstanceObj of dataEnv) {
662
+ const envPath = `${baseConfPath}/${deployId}/.env.${envInstanceObj.env}`;
663
+ const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
664
+ envObj.PORT = `${envInstanceObj.port + effectivePortOffset}`;
665
+ writeEnv(envPath, envObj);
666
+ }
667
+
668
+ if (singleReplicaPortOffsets[deployId] !== undefined) continue;
669
+
670
+ const serverConf = loadReplicas(
671
+ deployId,
672
+ loadConfServerJson(`${baseConfPath}/${deployId}/conf.server.json`),
673
+ );
674
+ for (const host of Object.keys(serverConf)) {
675
+ let deferredSingleReplicaSlots = [];
676
+ for (const path of Object.keys(serverConf[host])) {
677
+ if (serverConf[host][path].singleReplica && serverConf[host][path].replicas) {
678
+ deferredSingleReplicaSlots.push({
679
+ replicas: serverConf[host][path].replicas,
680
+ peer: !!serverConf[host][path].peer,
681
+ });
682
+ continue;
683
+ }
684
+ portOffset++;
685
+ if (serverConf[host][path].peer) portOffset++;
686
+ }
687
+ for (const slot of deferredSingleReplicaSlots) {
688
+ for (const replica of slot.replicas) {
689
+ const replicaDeployId = buildReplicaId({ deployId, replica });
690
+ singleReplicaPortOffsets[replicaDeployId] = portOffset;
691
+ portOffset++;
692
+ if (slot.peer) portOffset++;
693
+ }
694
+ }
695
+ }
696
+ }
697
+ return resolve(true);
698
+ }
699
+
700
+ // Handle singleReplica operation
701
+ if (options.singleReplica) {
702
+ const replicaPath = path;
703
+ if (!deployId || !host || !replicaPath) {
704
+ logger.error('client --single-replica requires deploy-id, host, and path arguments');
705
+ return reject(false);
706
+ }
707
+ const serverConf = loadReplicas(
708
+ deployId,
709
+ loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
710
+ );
711
+
712
+ if (serverConf[host][replicaPath].replicas) {
713
+ {
714
+ let replicaIndex = -1;
715
+ for (const replica of serverConf[host][replicaPath].replicas) {
716
+ replicaIndex++;
717
+ const replicaDeployId = `${deployId}-${serverConf[host][replicaPath].replicas[replicaIndex].slice(1)}`;
718
+ await fs.copy(`./engine-private/conf/${deployId}`, `./engine-private/replica/${replicaDeployId}`);
719
+ fs.writeFileSync(
720
+ `./engine-private/replica/${replicaDeployId}/package.json`,
721
+ fs
722
+ .readFileSync(`./engine-private/replica/${replicaDeployId}/package.json`, 'utf8')
723
+ .replaceAll(`${deployId}`, `${replicaDeployId}`),
724
+ 'utf8',
725
+ );
726
+ const replicaFolder = `./engine-private/replica/${replicaDeployId}`;
727
+ for (const envFile of ['.env.production', '.env.development', '.env.test']) {
728
+ const envFilePath = `${replicaFolder}/${envFile}`;
729
+ if (fs.existsSync(envFilePath)) {
730
+ fs.writeFileSync(
731
+ envFilePath,
732
+ fs
733
+ .readFileSync(envFilePath, 'utf8')
734
+ .replaceAll(`DEPLOY_ID=${deployId}`, `DEPLOY_ID=${replicaDeployId}`),
735
+ 'utf8',
736
+ );
737
+ }
738
+ }
739
+ }
740
+ }
741
+ {
742
+ let replicaIndex = -1;
743
+ for (const replica of serverConf[host][replicaPath].replicas) {
744
+ replicaIndex++;
745
+ const replicaDeployId = `${deployId}-${serverConf[host][replicaPath].replicas[replicaIndex].slice(1)}`;
746
+ let replicaServerConf = JSON.parse(
747
+ fs.readFileSync(`./engine-private/replica/${replicaDeployId}/conf.server.json`, 'utf8'),
748
+ );
749
+
750
+ const singleReplicaConf = replicaServerConf[host][replicaPath];
751
+ singleReplicaConf.replicas = undefined;
752
+ singleReplicaConf.singleReplica = undefined;
753
+
754
+ replicaServerConf = {};
755
+ replicaServerConf[host] = {};
756
+ replicaServerConf[host][replica] = singleReplicaConf;
757
+
758
+ fs.writeFileSync(
759
+ `./engine-private/replica/${replicaDeployId}/conf.server.json`,
760
+ JSON.stringify(replicaServerConf, null, 4),
761
+ 'utf8',
762
+ );
763
+ }
764
+ }
765
+ }
766
+ return resolve(true);
767
+ }
768
+
769
+ // Handle buildFullClient operation (default)
770
+ {
771
+ const { deployId: resolvedDeployId } = loadConf(deployId, subConf ?? '');
772
+
773
+ let argHost = host ? host.split(',') : [];
774
+ let argPath = path ? path.split(',') : [];
775
+ let deployIdSingleReplicas = [];
776
+ let singleReplicaHosts = [];
777
+ const serverConf = resolvedDeployId
778
+ ? readConfJson(resolvedDeployId, 'server', { loadReplicas: true })
779
+ : Config.default.server;
780
+ const confFilePath = resolvedDeployId ? getConfFilePath(resolvedDeployId, 'server') : null;
781
+ const originalConfBackup = confFilePath ? fs.readFileSync(confFilePath, 'utf8') : null;
782
+ for (const host of Object.keys(serverConf)) {
783
+ for (const path of Object.keys(serverConf[host])) {
784
+ if (argHost.length && argPath.length && (!argHost.includes(host) || !argPath.includes(path))) {
785
+ delete serverConf[host][path];
786
+ } else {
787
+ serverConf[host][path].liteBuild = false;
788
+ serverConf[host][path].minifyBuild = process.env.NODE_ENV === 'production' ? true : false;
789
+ if (serverConf[host][path].singleReplica && serverConf[host][path].replicas) {
790
+ singleReplicaHosts.push({ host, path });
791
+ deployIdSingleReplicas = deployIdSingleReplicas.concat(
792
+ serverConf[host][path].replicas.map((replica) =>
793
+ buildReplicaId({ deployId: resolvedDeployId, replica }),
794
+ ),
795
+ );
796
+ }
797
+ }
798
+ }
799
+ }
800
+ if (confFilePath) fs.writeFileSync(confFilePath, JSON.stringify(serverConf, null, 4), 'utf-8');
801
+ await buildClient();
802
+ if (confFilePath && originalConfBackup) fs.writeFileSync(confFilePath, originalConfBackup, 'utf-8');
803
+
804
+ if (singleReplicaHosts.length > 0) {
805
+ for (const { host, path } of singleReplicaHosts) {
806
+ await Underpost.repo.client(resolvedDeployId, '', host, path, {
807
+ singleReplica: true,
808
+ });
809
+ }
810
+ shellExec(`node bin env ${resolvedDeployId} ${process.env.NODE_ENV}`);
811
+ }
812
+
813
+ for (const replicaDeployId of deployIdSingleReplicas) {
814
+ shellExec(`node bin env ${replicaDeployId} ${process.env.NODE_ENV}`);
815
+ await Underpost.repo.client(replicaDeployId);
816
+ }
817
+ return resolve(true);
818
+ }
819
+ } catch (error) {
820
+ console.log(error);
821
+ logger.error(error, error.stack);
822
+ return reject(false);
823
+ }
824
+ });
825
+ },
826
+
575
827
  /**
576
828
  * Gets a list of deleted files from a Git repository.
577
829
  * @param {string} [path='.'] - The path to the repository.
@@ -720,25 +972,14 @@ Prevent build private config repo.`,
720
972
  DefaultConf.client = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.client.json`, 'utf8'));
721
973
  DefaultConf.server = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8'));
722
974
  DefaultConf.ssr = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.ssr.json`, 'utf8'));
723
- // DefaultConf.cron = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.cron.json`, 'utf8'));
724
-
725
- for (const host of Object.keys(DefaultConf.server)) {
726
- for (const path of Object.keys(DefaultConf.server[host])) {
727
- DefaultConf.server[host][path].db = defaultServer.db;
728
- DefaultConf.server[host][path].mailer = defaultServer.mailer;
729
-
730
- delete DefaultConf.server[host][path]._wp_client;
731
- delete DefaultConf.server[host][path]._wp_git;
732
- delete DefaultConf.server[host][path]._wp_directory;
733
- delete DefaultConf.server[host][path].wp;
734
- delete DefaultConf.server[host][path].git;
735
- delete DefaultConf.server[host][path].directory;
736
- }
737
- }
738
975
  } else
739
976
  logger.warn(
740
977
  `Deploy ID configuration not found: ./engine-private/conf/${deployId}, using default configuration.`,
741
978
  );
979
+
980
+ // Serialize the configuration into the conf.*.js manifest file.
981
+ // env: references from JSON configs are preserved as 'env:KEY' strings.
982
+ // At runtime, resolveConfSecrets() in conf.js resolves them via process.env.
742
983
  const sepRender = '/**/';
743
984
  const confRawPaths = fs.readFileSync('./conf.js', 'utf8').split(sepRender);
744
985
  confRawPaths[1] = `${JSON.stringify(DefaultConf)};`;
@@ -924,6 +1165,59 @@ Prevent build private config repo.`,
924
1165
 
925
1166
  return copiedFiles;
926
1167
  },
1168
+
1169
+ /**
1170
+ * Dispatches a GitHub Actions workflow using gh CLI or curl fallback.
1171
+ * @param {object} options - Dispatch options.
1172
+ * @param {string} options.repo - The GitHub repository (e.g., "owner/repo").
1173
+ * @param {string} options.workflowFile - The workflow file name (e.g., "engine-core.cd.yml").
1174
+ * @param {string} [options.ref='master'] - The git ref to dispatch against.
1175
+ * @param {object} [options.inputs={}] - Key-value inputs for the workflow_dispatch event.
1176
+ * @memberof UnderpostRepository
1177
+ */
1178
+ dispatchWorkflow(options = { repo: '', workflowFile: '', ref: 'master', inputs: {} }) {
1179
+ const { repo, workflowFile, ref, inputs } = options;
1180
+ const ghAvailable = shellExec('command -v gh 2>/dev/null', {
1181
+ stdout: true,
1182
+ silent: true,
1183
+ disableLog: true,
1184
+ }).trim();
1185
+
1186
+ if (ghAvailable) {
1187
+ let cmd = `gh workflow run ${workflowFile} --repo ${repo} --ref ${ref}`;
1188
+ for (const [key, value] of Object.entries(inputs)) {
1189
+ if (value !== undefined && value !== '') {
1190
+ const escaped = String(value).replace(/'/g, "'\\''");
1191
+ cmd += ` -f ${key}='${escaped}'`;
1192
+ }
1193
+ }
1194
+ shellExec(cmd);
1195
+ } else {
1196
+ let token = process.env.GITHUB_TOKEN;
1197
+ if (!token) {
1198
+ const envPath = `${getNpmRootPath()}/underpost/.env`;
1199
+ if (fs.existsSync(envPath) && fs.statSync(envPath).isFile()) {
1200
+ const envVars = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
1201
+ token = envVars.GITHUB_TOKEN;
1202
+ }
1203
+ }
1204
+ if (!token) {
1205
+ logger.error('GITHUB_TOKEN is required for workflow dispatch (gh CLI not available)');
1206
+ return;
1207
+ }
1208
+ const payload = { ref };
1209
+ if (Object.keys(inputs).length > 0) payload.inputs = inputs;
1210
+ const payloadJson = JSON.stringify(payload).replace(/'/g, "'\\''");
1211
+ shellExec(
1212
+ `curl -s -f -X POST ` +
1213
+ `-H "Accept: application/vnd.github.v3+json" ` +
1214
+ `-H "Authorization: token ${token}" ` +
1215
+ `"https://api.github.com/repos/${repo}/actions/workflows/${workflowFile}/dispatches" ` +
1216
+ `-d '${payloadJson}'`,
1217
+ );
1218
+ }
1219
+ logger.info('Dispatched workflow', `${repo} -> ${workflowFile}`, inputs.job ? `(job: ${inputs.job})` : '');
1220
+ },
927
1221
  };
928
1222
  }
929
1223