underpost 3.2.30 → 3.2.80

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 (46) hide show
  1. package/.github/workflows/ghpkg.ci.yml +87 -0
  2. package/.github/workflows/npmpkg.ci.yml +9 -6
  3. package/.github/workflows/publish.ci.yml +3 -3
  4. package/.github/workflows/pwa-microservices-template-page.cd.yml +0 -7
  5. package/.github/workflows/release.cd.yml +1 -1
  6. package/CHANGELOG.md +1230 -971
  7. package/CLI-HELP.md +14 -6
  8. package/README.md +3 -3
  9. package/bin/build.js +40 -4
  10. package/bin/deploy.js +2 -2
  11. package/bump.config.js +1 -0
  12. package/docker-compose.yml +26 -26
  13. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  14. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  15. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  16. package/package.json +15 -15
  17. package/scripts/disk-clean.sh +85 -60
  18. package/scripts/test-monitor.sh +1 -1
  19. package/src/api/core/core.controller.js +4 -65
  20. package/src/api/core/core.router.js +8 -14
  21. package/src/api/default/default.controller.js +2 -70
  22. package/src/api/default/default.router.js +7 -17
  23. package/src/api/document/document.controller.js +5 -77
  24. package/src/api/document/document.router.js +9 -13
  25. package/src/api/file/file.controller.js +9 -53
  26. package/src/api/file/file.router.js +14 -6
  27. package/src/api/test/test.controller.js +8 -53
  28. package/src/api/test/test.router.js +1 -4
  29. package/src/cli/cluster.js +104 -11
  30. package/src/cli/db.js +4 -2
  31. package/src/cli/deploy.js +60 -11
  32. package/src/cli/docker-compose.js +212 -1
  33. package/src/cli/fs.js +45 -25
  34. package/src/cli/image.js +147 -51
  35. package/src/cli/index.js +26 -6
  36. package/src/cli/release.js +50 -4
  37. package/src/cli/repository.js +98 -24
  38. package/src/cli/run.js +380 -178
  39. package/src/cli/secrets.js +75 -46
  40. package/src/cli/ssh.js +30 -11
  41. package/src/client/components/core/Modal.js +38 -4
  42. package/src/index.js +1 -1
  43. package/src/server/catalog.js +2 -0
  44. package/src/server/conf.js +163 -3
  45. package/src/server/downloader.js +3 -3
  46. package/src/server/middlewares.js +152 -0
package/src/cli/index.js CHANGED
@@ -108,18 +108,24 @@ program
108
108
  .option('--edit', 'Edit last commit.')
109
109
  .option('--deploy-id <deploy-id>', 'Sets the deployment configuration ID for the commit context.')
110
110
  .option('--cached', 'Commit staged changes only or context.')
111
- .option('--hashes <hashes>', 'Comma-separated list of specific file hashes of commits.')
112
- .option('--extension <extension>', 'specific file extensions of commits.')
113
111
  .option(
114
- '--changelog [latest-n]',
115
- 'Print plain the changelog of the specified number of latest n commits, if no number is provided it will get the changelog to latest ci integration',
112
+ '--init-repo [origin]',
113
+ 'Initialize a git repository at the specified path. Optionally set the git remote origin URL.',
116
114
  )
115
+ .option('--hashes <hashes>', 'Comma-separated list of specific file hashes of commits.')
116
+ .option('--extension <extension>', 'specific file extensions of commits.')
117
+ .option('--changelog', 'Print the plain changelog of the last N commits (see --from-n-commit, default 1).')
117
118
  .option('--changelog-build', 'Builds a CHANGELOG.md file based on the commit history')
118
119
  .option('--changelog-min-version <version>', 'Sets the minimum version limit for --changelog-build (default: 2.85.0)')
119
120
  .option(
120
121
  '--changelog-no-hash',
121
122
  'Excludes commit hashes from the generated changelog entries (used with --changelog-build).',
122
123
  )
124
+ .option(
125
+ '--changelog-msg',
126
+ 'Print the sanitized, commit-ready changelog message of the last N commits (see --from-n-commit, default 1). Empty when there are no tagged entries.',
127
+ )
128
+ .option('--from-n-commit <n>', 'Number of latest commits to include in --changelog/--changelog-msg (default: 1).')
123
129
  .option('--unpush', 'With --log, automatically sets range to unpushed commits ahead of remote.')
124
130
  .option('-b', 'Shows the current Git branch name.')
125
131
  .option('-p [branch]', 'Shows the reflog for the specified branch.')
@@ -434,25 +440,31 @@ program
434
440
  .option('--rm <image-id>', 'Removes specified Underpost Dockerfile images.')
435
441
  .option('--path [path]', 'The path to the Dockerfile directory.')
436
442
  .option('--image-name [image-name]', 'Sets a custom name for the Docker image.')
437
- .option('--image-path [image-path]', 'Sets the output path for the tar image archive.')
443
+ .option('--image-out-path [image-out-path]', 'Sets the output path for the tar image archive.')
438
444
  .option('--dockerfile-name [dockerfile-name]', 'Sets a custom name for the Dockerfile.')
439
445
  .option('--podman-save', 'Exports the built image as a tar file using Podman.')
440
- .option('--pull-base', 'Pulls base images and builds a "rockylinux9-underpost" image.')
446
+ .option('--pull-base', 'Pulls the base image prerequisites (rockylinux:9) on the host; combine with --build.')
441
447
  .option('--spec', 'Get current cached list of container images used by all pods')
442
448
  .option('--namespace <namespace>', 'Kubernetes namespace for image operations (defaults to "default").')
443
449
  .option('--kind', 'Set kind cluster env image context management.')
444
450
  .option('--kubeadm', 'Set kubeadm cluster env image context management.')
445
451
  .option('--k3s', 'Set k3s cluster env image context management.')
452
+ .option('--docker-compose', 'Load the built image tar into the local Docker store for Docker Compose availability.')
446
453
  .option('--node-name', 'Set node name for kubeadm or k3s cluster env image context management.')
447
454
  .option('--reset', 'Performs a build without using the cache.')
448
455
  .option('--dev', 'Use development mode.')
449
456
  .option('--pull-dockerhub <dockerhub-image>', 'Sets a custom Docker Hub image for base image pulls.')
457
+ .option(
458
+ '--import-tar <tar-path>',
459
+ 'Load a pre-built image tar archive (e.g. ./image-v1.0.0.tar) into the enabled target(s) without building. Combine with --kind, --kubeadm, --k3s and/or --docker-compose; the archive is loaded into each enabled one.',
460
+ )
450
461
  .description('Manages Docker images, including building, saving, and loading into Kubernetes clusters.')
451
462
  .action(async (options) => {
452
463
  if (options.rm) Underpost.image.rm({ ...options, imageName: options.rm });
453
464
  if (options.ls) Underpost.image.list({ ...options, log: true });
454
465
  if (options.pullBase) Underpost.image.pullBaseImages(options);
455
466
  if (options.build) Underpost.image.build(options);
467
+ if (options.importTar) Underpost.image.importTar(options);
456
468
  if (options.pullDockerhub)
457
469
  Underpost.image.pullDockerHubImage({ ...options, dockerhubImage: options.pullDockerhub });
458
470
  });
@@ -692,6 +704,7 @@ program
692
704
  .option('--kubeadm', 'Sets the kubeadm cluster context for the runner execution.')
693
705
  .option('--k3s', 'Sets the k3s cluster context for the runner execution.')
694
706
  .option('--kind', 'Sets the kind cluster context for the runner execution.')
707
+ .option('--traffic <traffic>', 'Blue/green traffic colour to bake into generated manifests (default: blue).')
695
708
  .option('--git-clean', 'Runs git clean on volume mount paths before copying.')
696
709
  .option('--deploy-id <deploy-id>', 'Sets deploy id context for the runner execution.')
697
710
  .option('--user <user>', 'Sets user context for the runner execution.')
@@ -749,6 +762,7 @@ program
749
762
  '--test',
750
763
  'Enables test/generic-purpose mode for the runner (e.g. use self-signed TLS instead of cert-manager).',
751
764
  )
765
+ .option('--branch <branch>', 'Sets the branch for git operations (default: current branch).')
752
766
  .description('Runs specified scripts using various runners.')
753
767
  .action(Underpost.run.callback);
754
768
 
@@ -765,6 +779,12 @@ program
765
779
  '--deploy-id <deploy-id>',
766
780
  "Deployment to run as the app container (default: dd-default). 'dd-default' self-bootstraps a fresh engine; any other id runs the standard 'underpost start' command (mirrors src/cli/deploy.js).",
767
781
  )
782
+ .option(
783
+ '--docker-compose-id <docker-compose-id>',
784
+ 'Selects a canonical custom-workflow stack at engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>/ ' +
785
+ '(docker-compose.yml + compose.env + nginx.conf, used as-is; nginx/env generation is skipped). ' +
786
+ 'e.g. --deploy-id dd-cyberia --docker-compose-id cyberia for the Cyberia MMO ecosystem.',
787
+ )
768
788
  .option('--env <env>', 'Deployment environment for non-default deploy ids (default: development).')
769
789
  .option('--generate', 'Render dynamic supporting files (nginx router config, env-file, app-command override).')
770
790
  .option('--up', 'Start the full stack detached (regenerates config first).')
@@ -117,9 +117,57 @@ const buildVersionBumpTargets = () => [
117
117
  /(underpost-engine:v)\d+\.\d+\.\d+/g,
118
118
  /(type=raw,value=v)\d+\.\d+\.\d+/g,
119
119
  /(UNDERPOST_VERSION=)\d+\.\d+\.\d+/g,
120
+ /(UNDERPOST_VERSION:\s*['"]?)\d+\.\d+\.\d+/g,
120
121
  ],
121
122
  },
122
123
 
124
+ // ── Docker-compose image-tag defaults. Root compose + generator + cyberia runtime compose.
125
+ // Tags live in `${VAR:-vX.Y.Z}` / `VAR=vX.Y.Z` shapes bumpp cannot detect. ──
126
+ {
127
+ file: 'docker-compose.yml',
128
+ patterns: /(_TAG:-v)\d+\.\d+\.\d+/g,
129
+ },
130
+ {
131
+ file: 'src/cli/docker-compose.js',
132
+ patterns: /(_TAG=v)\d+\.\d+\.\d+/g,
133
+ },
134
+ {
135
+ file: 'src/runtime/engine-cyberia/docker-compose.yml',
136
+ patterns: /(_TAG:-v)\d+\.\d+\.\d+/g,
137
+ },
138
+ {
139
+ file: 'engine-private/conf/dd-cyberia/docker-compose/cyberia/docker-compose.yml',
140
+ patterns: /(_TAG:-v)\d+\.\d+\.\d+/g,
141
+ },
142
+
143
+ // ── Cyberia CLI dev image tar/name defaults (bin/cyberia.js). ──
144
+ {
145
+ file: 'bin/cyberia.js',
146
+ patterns: [/(-dev_v)\d+\.\d+\.\d+(?=\.tar)/g, /(-dev:v)\d+\.\d+\.\d+/g],
147
+ },
148
+
149
+ // ── Runtime Dockerfiles: `ARG UNDERPOST_VERSION=X.Y.Z` build-arg defaults. ──
150
+ {
151
+ dir: 'src/runtime',
152
+ match: /^Dockerfile(\.\w+)?$/,
153
+ patterns: /(ARG UNDERPOST_VERSION=)\d+\.\d+\.\d+/g,
154
+ recursive: true,
155
+ },
156
+
157
+ // ── Runtime compose.env shipped tag defaults ──
158
+ {
159
+ dir: 'src/runtime',
160
+ match: /^compose\.env$/,
161
+ patterns: /(_TAG=v)\d+\.\d+\.\d+/g,
162
+ recursive: true,
163
+ },
164
+
165
+ // ── Deploy-monitor smoke-test image default (scripts/test-monitor.sh). ──
166
+ {
167
+ file: 'scripts/test-monitor.sh',
168
+ patterns: /(underpost\/[a-z0-9-]+:v)\d+\.\d+\.\d+/g,
169
+ },
170
+
123
171
  // ── engine-private confs (gitignored — bumped only if present). ──
124
172
  {
125
173
  dir: 'engine-private/conf',
@@ -448,11 +496,10 @@ class UnderpostRelease {
448
496
  let commitMsg = message;
449
497
  if (!commitMsg) {
450
498
  shellCd('/home/dd/engine');
451
- const rawMsg = shellExec(`node bin cmt --changelog 1 --changelog-no-hash`, {
499
+ commitMsg = shellExec(`node bin cmt --changelog-msg --changelog-no-hash`, {
452
500
  stdout: true,
453
501
  silent: true,
454
502
  }).trim();
455
- commitMsg = Underpost.repo.sanitizeChangelogMessage(rawMsg);
456
503
  shellCd('/home/dd');
457
504
  }
458
505
  commitMsg = (commitMsg || '').trim() || `Update ${repoName} repository`;
@@ -492,11 +539,10 @@ class UnderpostRelease {
492
539
  let commitMsg = message;
493
540
  if (!commitMsg) {
494
541
  shellCd('/home/dd/engine');
495
- const rawMsg = shellExec(`node bin cmt --changelog 1 --changelog-no-hash`, {
542
+ commitMsg = shellExec(`node bin cmt --changelog-msg --changelog-no-hash`, {
496
543
  stdout: true,
497
544
  silent: true,
498
545
  }).trim();
499
- commitMsg = Underpost.repo.sanitizeChangelogMessage(rawMsg);
500
546
  }
501
547
  commitMsg = (commitMsg || '').trim() || `Update pwa-microservices-template repository`;
502
548
  shellCd('/home/dd');
@@ -21,6 +21,7 @@ import {
21
21
  getDataDeploy,
22
22
  buildReplicaId,
23
23
  writeEnv,
24
+ readConfInstances,
24
25
  } from '../server/conf.js';
25
26
  import { buildClient, unzipClientBuild, mergeClientBuildZip } from '../client-builder/client-build.js';
26
27
  import { DefaultConf } from '../../conf.js';
@@ -145,6 +146,14 @@ class UnderpostRepository {
145
146
  ) {
146
147
  if (!repoPath) repoPath = '.';
147
148
 
149
+ if (options.initRepo) {
150
+ Underpost.repo.initLocalRepo({
151
+ path: repoPath,
152
+ origin: typeof options.initRepo === 'string' ? options.initRepo : undefined,
153
+ });
154
+ return;
155
+ }
156
+
148
157
  if (options.hasChanges) {
149
158
  const status = shellExec(`cd ${repoPath} && git status --porcelain`, {
150
159
  stdout: true,
@@ -218,7 +227,7 @@ class UnderpostRepository {
218
227
  return;
219
228
  }
220
229
 
221
- if (options.changelog !== undefined || options.changelogBuild) {
230
+ if (options.changelog !== undefined || options.changelogBuild || options.changelogMsg !== undefined) {
222
231
  const releaseMatch = 'New release v:';
223
232
  // Helper: parse [<tag>] commits into grouped sections
224
233
  const buildSectionChangelog = (commits) => {
@@ -255,7 +264,8 @@ class UnderpostRepository {
255
264
  stdout: true,
256
265
  silent: true,
257
266
  disableLog: true,
258
- });
267
+ silentOnError: true,
268
+ }).toString();
259
269
  return rawLog
260
270
  .split('\n')
261
271
  .map((line) => {
@@ -346,15 +356,18 @@ class UnderpostRepository {
346
356
  fs.writeFileSync(changelogPath, `# Changelog\n\n${changelog}`);
347
357
  logger.info('CHANGELOG.md built at', changelogPath);
348
358
  } else {
349
- // --changelog [latest-n]: print changelog of last N commits (default: 1)
350
- const hasExplicitCount =
351
- options.changelog !== undefined && options.changelog !== true && !isNaN(parseInt(options.changelog));
352
- const scanLimit = hasExplicitCount ? parseInt(options.changelog) : 1;
353
- const allCommits = fetchHistory(scanLimit);
354
-
355
- const sections = buildVersionSections(allCommits);
356
- let changelog = renderSections(sections);
357
- console.log(changelog || `No changelog entries found.\n`);
359
+ // --changelog / --changelog-msg: message from the last N commits, where N is --from-n-commit
360
+ // (default 1, last commit only). No auto-detection.
361
+ const n = parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
362
+ const sections = buildVersionSections(fetchHistory(n));
363
+ const changelog = renderSections(sections);
364
+ if (options.changelogMsg !== undefined) {
365
+ // Sanitized, commit-ready message; empty string when there are no tagged entries so
366
+ // callers fall back to their own generic default instead of a placeholder.
367
+ console.log(Underpost.repo.sanitizeChangelogMessage(changelog));
368
+ } else {
369
+ console.log(changelog || `No changelog entries found.\n`);
370
+ }
358
371
  }
359
372
 
360
373
  return;
@@ -1257,17 +1270,40 @@ Prevent build private config repo.`,
1257
1270
  return copiedFiles;
1258
1271
  },
1259
1272
 
1273
+ /**
1274
+ * Resolves the default branch for a remote GitHub repository by querying
1275
+ * `git ls-remote --symref` and extracting the HEAD ref target.
1276
+ * Falls back to `main` when detection fails.
1277
+ * @param {string} repo - The GitHub repository (e.g., "owner/repo").
1278
+ * @returns {string} The default branch name (e.g. "main" or "master").
1279
+ * @memberof UnderpostRepository
1280
+ */
1281
+ getDefaultBranch(repo) {
1282
+ if (!repo) return 'main';
1283
+ const authUrl = Underpost.repo.resolveAuthUrl(repo);
1284
+ const raw = shellExec(
1285
+ `GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote --symref "${authUrl}" HEAD 2>&1`,
1286
+ { stdout: true, silent: true, disableLog: true, silentOnError: true },
1287
+ );
1288
+ // --symref emits a line like: ref: refs/heads/main HEAD
1289
+ const match = typeof raw === 'string' ? raw.match(/^ref:\s*refs\/heads\/(\S+)\tHEAD$/m) : null;
1290
+ const branch = match ? match[1] : 'main';
1291
+ logger.info('getDefaultBranch', { repo, branch });
1292
+ return branch;
1293
+ },
1294
+
1260
1295
  /**
1261
1296
  * Dispatches a GitHub Actions workflow using gh CLI or curl fallback.
1262
1297
  * @param {object} options - Dispatch options.
1263
1298
  * @param {string} options.repo - The GitHub repository (e.g., "owner/repo").
1264
1299
  * @param {string} options.workflowFile - The workflow file name (e.g., "engine-core.cd.yml").
1265
- * @param {string} [options.ref='master'] - The git ref to dispatch against.
1300
+ * @param {string} [options.ref] - The git ref to dispatch against. Auto-detects the remote's default branch when omitted.
1266
1301
  * @param {object} [options.inputs={}] - Key-value inputs for the workflow_dispatch event.
1267
1302
  * @memberof UnderpostRepository
1268
1303
  */
1269
- dispatchWorkflow(options = { repo: '', workflowFile: '', ref: 'master', inputs: {} }) {
1270
- const { repo, workflowFile, ref, inputs } = options;
1304
+ dispatchWorkflow(options = { repo: '', workflowFile: '', ref: '', inputs: {} }) {
1305
+ const { repo, workflowFile, inputs } = options;
1306
+ const ref = options.ref || Underpost.repo.getDefaultBranch(repo);
1271
1307
  const ghAvailable = shellExec('command -v gh 2>/dev/null', {
1272
1308
  stdout: true,
1273
1309
  silent: true,
@@ -1344,14 +1380,18 @@ Prevent build private config repo.`,
1344
1380
  if (!url) return false;
1345
1381
  const authUrl = Underpost.repo.resolveAuthUrl(url);
1346
1382
  // GIT_TERMINAL_PROMPT=0 prevents git from hanging on credential prompts inside containers.
1347
- const raw = shellExec(`GIT_TERMINAL_PROMPT=0 git ls-remote "${authUrl}" HEAD 2>&1`, {
1383
+ // `-c credential.helper=` disables any host-configured credential helper so its stderr
1384
+ // warnings don't pollute the ls-remote output; the token is already embedded in authUrl.
1385
+ const raw = shellExec(`GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote "${authUrl}" HEAD 2>&1`, {
1348
1386
  stdout: true,
1349
1387
  silent: true,
1350
1388
  disableLog: true,
1351
1389
  silentOnError: true,
1352
1390
  });
1353
- logger.info('isRemoteRepo', { url, raw: (raw || '').slice(0, 120) });
1354
- return typeof raw === 'string' && /^[0-9a-f]{40}\t/m.test(raw);
1391
+ const refLine = typeof raw === 'string' ? (raw.match(/^[0-9a-f]{40}\t.*$/m) || [])[0] : undefined;
1392
+ const accessible = !!refLine;
1393
+ logger.info('isRemoteRepo', { url, accessible, ref: refLine || (raw || '').trim().split('\n').pop() });
1394
+ return accessible;
1355
1395
  },
1356
1396
 
1357
1397
  /**
@@ -1422,17 +1462,31 @@ Prevent build private config repo.`,
1422
1462
  * @memberof UnderpostRepository
1423
1463
  */
1424
1464
  getUnpushedCount(repoPath = '.', fallback = 1) {
1465
+ // Every git call is silentOnError: a detached HEAD (CI checkout) or a missing upstream must
1466
+ // degrade to the fallback, never throw — otherwise the thrown error is logged to stdout and
1467
+ // can be captured as a commit message by callers that read this command's output.
1425
1468
  const branch = shellExec(`cd ${repoPath} && git branch --show-current`, {
1426
1469
  stdout: true,
1427
1470
  silent: true,
1428
1471
  disableLog: true,
1429
- }).trim();
1430
- shellExec(`cd ${repoPath} && git fetch origin 2>/dev/null`, { silent: true, disableLog: true });
1472
+ silentOnError: true,
1473
+ })
1474
+ .toString()
1475
+ .trim();
1476
+ if (!branch) return { count: fallback, branch: '', hasUnpushed: false };
1477
+ shellExec(`cd ${repoPath} && git fetch origin 2>/dev/null`, {
1478
+ silent: true,
1479
+ disableLog: true,
1480
+ silentOnError: true,
1481
+ });
1431
1482
  const raw = shellExec(`cd ${repoPath} && git rev-list --count origin/${branch}..HEAD 2>/dev/null`, {
1432
1483
  stdout: true,
1433
1484
  silent: true,
1434
1485
  disableLog: true,
1435
- }).trim();
1486
+ silentOnError: true,
1487
+ })
1488
+ .toString()
1489
+ .trim();
1436
1490
  const count = parseInt(raw);
1437
1491
  const hasUnpushed = !isNaN(count) && count > 0;
1438
1492
  return { count: hasUnpushed ? count : fallback, branch, hasUnpushed };
@@ -1447,7 +1501,7 @@ Prevent build private config repo.`,
1447
1501
  */
1448
1502
  sanitizeChangelogMessage(message) {
1449
1503
  if (!message) return '';
1450
- return message
1504
+ const sanitized = message
1451
1505
  .replace(/^##\s+\d{4}-\d{2}-\d{2}\s*/gm, '')
1452
1506
  .replace(/^###\s+(\S+)\s*/gm, '[$1] ')
1453
1507
  .replace(/^- /gm, '')
@@ -1459,6 +1513,9 @@ Prevent build private config repo.`,
1459
1513
  .join('\n')
1460
1514
  .trim()
1461
1515
  .replaceAll('] - ', '] ');
1516
+ // The empty-changelog placeholder must never become a commit message; return empty so
1517
+ // callers fall back to their own generic default.
1518
+ return sanitized === 'No changelog entries found.' ? '' : sanitized;
1462
1519
  },
1463
1520
  /**
1464
1521
  * Initializes a git repository at the given path and configures user identity
@@ -1783,12 +1840,17 @@ Prevent build private config repo.`,
1783
1840
  * 4. Falls back to `${GITHUB_USERNAME}/engine` when no match is found.
1784
1841
  *
1785
1842
  * @param {string} [runtime=''] - The runtime identifier to look up (e.g. `'cyberia-server'`, `'cyberia-client'`).
1843
+ * @param {boolean} [ownRuntimeRepo=false] - Whether to check for the runtime's own repository.
1786
1844
  * @returns {string} The resolved `owner/repo` string.
1787
1845
  * @memberof UnderpostRepository
1788
1846
  */
1789
- resolveInstanceRepo(runtime = '') {
1847
+ resolveInstanceRepo(runtime = '', ownRuntimeRepo = false) {
1790
1848
  const fallback = `${process.env.GITHUB_USERNAME}/engine`;
1791
1849
  if (!runtime) return fallback;
1850
+ // A `.dev` suffix selects the development image workflow
1851
+ // (docker-image.<runtime>.dev.ci.yml) but resolves to the same instance
1852
+ // repo as its production counterpart, so strip it before matching.
1853
+ runtime = runtime.replace(/\.dev$/, '');
1792
1854
  const ddRouter = './engine-private/deploy/dd.router';
1793
1855
  const deployIds = fs.existsSync(ddRouter)
1794
1856
  ? fs
@@ -1801,8 +1863,10 @@ Prevent build private config repo.`,
1801
1863
  const confPath = `./engine-private/conf/${deployId}/conf.instances.json`;
1802
1864
  if (!fs.existsSync(confPath)) continue;
1803
1865
  try {
1804
- const instances = JSON.parse(fs.readFileSync(confPath, 'utf8'));
1805
- const match = instances.find((i) => i && i.runtime === runtime);
1866
+ // readConfInstances returns the bare array of entries. The template
1867
+ // entries carry runtime + metadata.repository, so the un-expanded list
1868
+ // is enough here (no need to expand variants).
1869
+ const match = readConfInstances(deployId).find((i) => i && i.runtime === runtime);
1806
1870
  if (match && match.metadata && match.metadata.repository) {
1807
1871
  logger.info(`[resolveInstanceRepo] resolved from ${confPath}`, {
1808
1872
  runtime,
@@ -1814,6 +1878,16 @@ Prevent build private config repo.`,
1814
1878
  logger.warn(`[resolveInstanceRepo] failed to parse ${confPath}: ${err.message}`);
1815
1879
  }
1816
1880
  }
1881
+ if (ownRuntimeRepo) {
1882
+ const runtimeRepo = Underpost.repo.isRemoteRepo(`${process.env.GITHUB_USERNAME}/${runtime}`);
1883
+ if (runtimeRepo) {
1884
+ logger.info(`[resolveInstanceRepo] resolved from ${process.env.GITHUB_USERNAME}/${runtime}`, {
1885
+ runtime,
1886
+ repo: `${process.env.GITHUB_USERNAME}/${runtime}`,
1887
+ });
1888
+ return `${process.env.GITHUB_USERNAME}/${runtime}`;
1889
+ }
1890
+ }
1817
1891
  return fallback;
1818
1892
  },
1819
1893