@underpostnet/underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
592 lines (554 loc) • 26 kB
JavaScript
/**
* Release orchestrator module for managing version builds and deployments of the Underpost CLI.
*
* Provides automated workflows for building new versions (bumping version numbers across
* all package files, manifests, and configurations) and deploying releases (committing,
* pushing, and syncing secrets to remote repositories).
*
* @module src/cli/release.js
* @namespace UnderpostRelease
*/
import fs from 'fs-extra';
import path from 'path';
import dotenv from 'dotenv';
import { pbcopy, shellCd, shellExec } from '../server/process.js';
import { Dns } from '../server/dns.js';
import { loggerFactory } from '../server/logger.js';
import { timer } from '../client/components/core/CommonJs.js';
import Underpost from '../index.js';
const logger = loggerFactory(import.meta);
/**
* Files that must never be touched by a version bump, even if they happen to contain a
* literal version string. CHANGELOG is historical; logs/ and node_modules/ are runtime
* outputs; engine-private/.env.* and secrets must not be rewritten by regex.
*/
const VERSION_BUMP_SKIP = [
/(^|\/)CHANGELOG\.md$/i,
/(^|\/)logs\//,
/(^|\/)node_modules\//,
/(^|\/)\.git\//,
/(^|\/)engine-private\/.*\.env(\..*)?$/,
];
const isSkipped = (filePath) => VERSION_BUMP_SKIP.some((re) => re.test(filePath));
/**
* Declarative regex-based version bump targets for files bumpp cannot handle natively
* (image tags, doc strings, README badges, build-args, etc.).
*
* Each target is one of:
* - { file: 'path/to/file', patterns: RegExp | RegExp[] }
* - { dir: 'dir', match: RegExp, patterns: RegExp | RegExp[], recursive?: boolean }
*
* Every pattern MUST have exactly one capture group: the prefix to preserve. The replacement
* is `$1<newVersion>`. Use a lookahead `(?=...)` to anchor a trailing context (closing quote,
* backtick, etc.) without consuming it. This keeps the callback signature trivial.
*
* The walker iterates targets, applies patterns to each matching file, and reports the
* number of substitutions per file. Files that match nothing are silently skipped.
*/
const buildVersionBumpTargets = () => [
// ── src/index.js — anchored to the static class field, never a bare replaceAll. ──
{
file: 'src/index.js',
patterns: /(static version = ['"]v)\d+\.\d+\.\d+(?=['"])/g,
},
// ── README + CLI-HELP fallbacks. CLI-HELP is regenerated by `deploy cli-docs`, but bump
// it too so the file is consistent if the regeneration step is skipped. ──
{
file: 'README.md',
patterns: [
/(socket\.dev\/api\/badge\/npm\/package\/underpost\/)\d+\.\d+\.\d+/g,
/(socket\.dev\/npm\/package\/underpost\/overview\/)\d+\.\d+\.\d+/g,
/(ci\/cd cli v)\d+\.\d+\.\d+/gi,
],
},
{
file: 'CLI-HELP.md',
patterns: /(ci\/cd cli v)\d+\.\d+\.\d+/gi,
optional: true,
},
// ── Cyberia + nexodev docs. ──
{
dir: 'src/client/public/cyberia-docs',
match: /\.md$/,
patterns: [/(\*\*(?:Current )?[Vv]ersion:\*\* )\d+\.\d+\.\d+/g, /(underpost\/[a-z0-9-]+:v)\d+\.\d+\.\d+/g],
recursive: true,
},
{
dir: 'src/client/public/nexodev/docs/references',
match: /\.md$/,
patterns: [
/(underpost\/[a-z0-9-]+:v)\d+\.\d+\.\d+/g,
/(UNDERPOST_VERSION=)\d+\.\d+\.\d+/g,
/(ci\/cd cli v)\d+\.\d+\.\d+/gi,
// version tag bumps inside markdown narrative, e.g. `v3.2.9`, `v3.2.10`, …
/(`v)\d+\.\d+\.\d+(?=`)/g,
],
recursive: true,
},
// ── Kubernetes manifests. Covers deployment.yaml + cronjob yamls under dd-cron. ──
{
dir: 'manifests/deployment',
match: /deployment\.yaml$/,
patterns: [/(underpost\/[a-z0-9-]+:v)\d+\.\d+\.\d+/g, /(engine\.version: )\d+\.\d+\.\d+/g],
recursive: true,
},
{
dir: 'manifests/cronjobs',
match: /\.yaml$/,
patterns: /(underpost\/[a-z0-9-]+:v)\d+\.\d+\.\d+/g,
recursive: true,
},
// ── GitHub Actions workflows. Single sweep across docker-image.*.ci.yml, engine-*.cd.yml,
// and the root docker-image.ci.yml — replaces previous three separate loops. ──
{
dir: '.github/workflows',
match: /\.(yml|yaml)$/,
patterns: [
/(underpost\/[a-z0-9-]+:v)\d+\.\d+\.\d+/g,
/(underpost-engine:v)\d+\.\d+\.\d+/g,
/(type=raw,value=v)\d+\.\d+\.\d+/g,
/(UNDERPOST_VERSION=)\d+\.\d+\.\d+/g,
/(UNDERPOST_VERSION:\s*['"]?)\d+\.\d+\.\d+/g,
],
},
// ── Docker-compose image-tag defaults. Root compose + generator + cyberia runtime compose.
// Tags live in `${VAR:-vX.Y.Z}` / `VAR=vX.Y.Z` shapes bumpp cannot detect. ──
{
file: 'docker-compose.yml',
patterns: /(_TAG:-v)\d+\.\d+\.\d+/g,
},
{
file: 'src/cli/docker-compose.js',
patterns: /(_TAG=v)\d+\.\d+\.\d+/g,
},
{
file: 'src/runtime/engine-cyberia/docker-compose.yml',
patterns: /(_TAG:-v)\d+\.\d+\.\d+/g,
},
// ── Cyberia CLI dev image tar/name defaults (bin/cyberia.js). ──
{
file: 'bin/cyberia.js',
patterns: [/(-dev_v)\d+\.\d+\.\d+(?=\.tar)/g, /(-dev:v)\d+\.\d+\.\d+/g],
},
// ── Runtime Dockerfiles: `ARG UNDERPOST_VERSION=X.Y.Z` build-arg defaults. ──
{
dir: 'src/runtime',
match: /^Dockerfile(\.\w+)?$/,
patterns: /(ARG UNDERPOST_VERSION=)\d+\.\d+\.\d+/g,
recursive: true,
},
// ── Runtime compose.env shipped tag defaults ──
{
dir: 'src/runtime',
match: /^compose\.env$/,
patterns: /(_TAG=v)\d+\.\d+\.\d+/g,
recursive: true,
},
// ── Deploy-monitor smoke-test image default (scripts/test-monitor.sh). ──
{
file: 'scripts/test-monitor.sh',
patterns: /(underpost\/[a-z0-9-]+:v)\d+\.\d+\.\d+/g,
},
// ── engine-private confs (gitignored — bumped only if present). ──
{
dir: 'engine-private/conf',
match: /(?:deployment\.yaml|conf\.instances\.json)$/,
patterns: [
/(underpost\/[a-z0-9-]+:v)\d+\.\d+\.\d+/g,
/(underpost-engine:v)\d+\.\d+\.\d+/g,
/(engine\.version: )\d+\.\d+\.\d+/g,
/(v)\d+\.\d+\.\d+/g,
],
recursive: true,
optional: true,
},
];
/**
* Walks a directory (optionally recursively) and returns paths matching `match`.
*/
const walkDir = (dir, match, recursive) => {
if (!fs.existsSync(dir)) return [];
const out = [];
const stack = [dir];
while (stack.length) {
const cur = stack.pop();
let entries;
try {
entries = fs.readdirSync(cur, { withFileTypes: true });
} catch {
continue;
}
for (const ent of entries) {
const full = path.join(cur, ent.name);
if (ent.isDirectory()) {
if (recursive) stack.push(full);
} else if (ent.isFile() && match.test(ent.name) && !isSkipped(full)) {
out.push(full);
}
}
}
return out;
};
/**
* Applies an array of regex patterns to a single file in place. Substitutes only when the
* matched version literally equals `oldVersion` — narrative references to other versions
* (e.g. example tags `(v3.2.9, v3.2.10, …)`) are left untouched.
*
* Returns the substitution count (0 if nothing matched).
*/
const bumpFile = (filePath, patterns, oldVersion, newVersion, { dryRun }) => {
if (isSkipped(filePath)) return 0;
if (!fs.existsSync(filePath)) return 0;
const original = fs.readFileSync(filePath, 'utf8');
const regexes = Array.isArray(patterns) ? patterns : [patterns];
let updated = original;
let count = 0;
for (const re of regexes) {
updated = updated.replace(re, (m, prefix) => {
const matchedVersion = m.match(/\d+\.\d+\.\d+/)?.[0];
if (matchedVersion !== oldVersion) return m;
count += 1;
return `${prefix}${newVersion}`;
});
}
if (count > 0 && updated !== original && !dryRun) {
fs.writeFileSync(filePath, updated, 'utf8');
}
return count;
};
/**
* Bumps every non-canonical file declared in VERSION_BUMP_TARGETS.
* Returns a per-file report so callers can print a summary.
*
* @param {string} oldVersion - The version currently in the files (pre-bump).
* @param {string} newVersion - The version to write.
* @param {{dryRun?: boolean}} [opts]
* @returns {Array<{file: string, count: number}>}
*/
const bumpAuxiliaryFiles = (oldVersion, newVersion, { dryRun = false } = {}) => {
const report = [];
if (oldVersion === newVersion) return report;
for (const target of buildVersionBumpTargets()) {
const files = target.file ? [target.file] : walkDir(target.dir, target.match, target.recursive ?? false);
for (const file of files) {
if (target.optional && !fs.existsSync(file)) continue;
const count = bumpFile(file, target.patterns, oldVersion, newVersion, { dryRun });
if (count > 0) report.push({ file, count });
}
}
return report;
};
/**
* Prints a per-file change summary table.
*/
const printBumpReport = (report, { dryRun }) => {
const header = dryRun ? 'Version bump (dry-run) — would change:' : 'Version bump — files updated:';
logger.info(header);
if (!report.length) {
console.log(' (no files matched)');
return;
}
const maxLen = Math.max(...report.map((r) => r.file.length));
for (const { file, count } of report) {
console.log(` ${file.padEnd(maxLen + 2)} ${count} match${count === 1 ? '' : 'es'}`);
}
console.log(` Total: ${report.length} file(s), ${report.reduce((s, r) => s + r.count, 0)} substitution(s)`);
};
/**
* Kills any Node.js dev-server or nodemon processes that may hold file locks
* (e.g. overwriting package.json). Skips VSCode internals and the current process.
*/
function killDevServers() {
for (const port of [4001, 4002, 4003, 3000]) shellExec(`node bin run kill ${port}`);
}
const TEMPLATE_PATH = '../pwa-microservices-template';
/**
* Runs a command in an ISOLATED environment. `release build` loads the engine's
* `dd-cron/.env.production` (DEPLOY_ID=dd-cron, DB creds, secrets, …) into its own `process.env`,
* which `shellExec` children would otherwise inherit — and the template's `dotenv.config()` runs
* without override, so it could never reclaim those keys. `env -i` starts the child with an empty
* environment; only PATH/HOME-class essentials are re-added so node/npm/git still resolve. The
* template then reads only its own `.env`, resolving `dd-default` exactly like a fresh clone.
*/
const ISOLATED_ENV = 'env -i HOME="$HOME" PATH="$PATH" USER="$USER" LOGNAME="$LOGNAME" TERM="$TERM" LANG="$LANG"';
/**
* Builds the pwa-microservices-template from scratch and smoke-tests its default workflow.
*
* 1. Cleans the engine, pulls latest, and rebuilds the template via `bin/build.template`.
* 2. Derives `.env` + `.env.example` from the template `.env.example` (DHCP host IP + file
* logs), both written from the same `dd-default` content so the default startup workflow
* bootstraps `engine-private` from scratch out of them. `.env` is rewritten because it is
* gitignored (git clean never removes it) and may otherwise hold a stale `dd-cron`.
* 3. Installs deps, then builds + runs the template `dev` server in an ISOLATED env
* (see `ISOLATED_ENV`) so it resolves `dd-default` like a fresh clone, and asserts the
* startup log is error-free.
*
* @returns {boolean} true when the template started cleanly, false otherwise.
*/
async function buildAndTestTemplate(opts = {}) {
killDevServers();
Underpost.repo.clean({ paths: ['/home/dd/engine', '/home/dd/engine/engine-private '] });
shellExec(`node bin pull . ${process.env.GITHUB_USERNAME}/engine`);
fs.removeSync(TEMPLATE_PATH);
shellExec(`npm run build:template`);
shellExec(`node bin run shared-dir ${TEMPLATE_PATH}`);
const upsertEnvVar = (content, key, value) => {
const re = new RegExp(`^(${key}=).*`, 'm');
if (re.test(content)) return content.replace(re, `$1${value}`);
return `${content.trimEnd()}\n${key}=${value}\n`;
};
const dhcpHostIp = Dns.getLocalIPv4Address();
logger.info(`DHCP host IP for template test: ${dhcpHostIp}`);
let envContent = fs.readFileSync(`${TEMPLATE_PATH}/.env.example`, 'utf8');
if (dhcpHostIp) envContent = envContent.replace(/127\.0\.0\.1/g, dhcpHostIp);
envContent = upsertEnvVar(envContent, 'ENABLE_FILE_LOGS', 'true');
if (opts.mongoHost) envContent = upsertEnvVar(envContent, 'DB_HOST', opts.mongoHost);
if (opts.mongoUser) envContent = upsertEnvVar(envContent, 'DB_USER', opts.mongoUser);
if (opts.mongoPassword) envContent = upsertEnvVar(envContent, 'DB_PASSWORD', opts.mongoPassword);
if (opts.valkeyHost) envContent = upsertEnvVar(envContent, 'VALKEY_HOST', opts.valkeyHost);
// fs.writeFileSync(`${TEMPLATE_PATH}/.env`, envContent, 'utf8');
fs.writeFileSync(`${TEMPLATE_PATH}/.env.example`, envContent, 'utf8');
shellExec(`cd ${TEMPLATE_PATH} && npm install`);
shellExec(`cd ${TEMPLATE_PATH} && node bin env clean`);
// Build + run in an isolated env so the template resolves dd-default from its own .env and
// never inherits the engine's dd-cron deploy selection. See ISOLATED_ENV above.
//
// ENABLE_FILE_LOGS must be passed inline: src/server.js builds start.js's logger at import
// time, before Config.build() loads the template .env, so the var has to be present in the
// process env from the start for logs/start.js/all.log (the success probe below) to be written.
shellExec(`cd ${TEMPLATE_PATH} && ${ISOLATED_ENV} npm run build`);
shellExec(`cd ${TEMPLATE_PATH} && ${ISOLATED_ENV} ENABLE_FILE_LOGS=true timeout 5s npm run dev`, { async: true });
await timer(5500);
const templateLogPath = `${TEMPLATE_PATH}/logs/start.js/all.log`;
const runnerResult = fs.existsSync(templateLogPath) ? fs.readFileSync(templateLogPath, 'utf8') : '';
logger.info('Test template runner result');
killDevServers();
shellCd(`/home/dd/engine`);
Underpost.repo.clean({ paths: ['/home/dd/engine', '/home/dd/engine/engine-private '] });
if (!runnerResult || runnerResult.toLowerCase().match('error')) {
logger.error('Test template runner result failed');
return false;
}
return true;
}
/**
* @class UnderpostRelease
* @description Orchestrates version builds and release deployments for the Underpost CLI.
* This class provides static methods to automate the full release lifecycle:
* building a new version (testing, bumping versions, rebuilding manifests)
* and deploying a release (syncing secrets, committing, and pushing to remotes).
* @memberof UnderpostRelease
*/
class UnderpostRelease {
static API = {
/**
* Builds a new version of the Underpost engine.
*
* Performs the full version build pipeline:
* 1. Loads production env and pulls latest code; kills dev servers on ports 4001-4003.
* 2. Builds and smoke-tests the pwa-microservices-template (aborts on error).
* 3. Canonical version files: delegates to `bumpp` (driven by `bump.config.js`). Bumps
* package.json, package-lock.json (root + packages['']), and every
* engine-private/conf/**\/package.json.
* 4. Auxiliary files: anchored-regex walker over VERSION_BUMP_TARGETS — covers
* src/index.js, README, cyberia-docs, nexodev docs, manifests (deployment + cronjobs),
* .github/workflows (image tags, type=raw, UNDERPOST_VERSION build-args), and
* engine-private confs (deployment.yaml + conf.instances.json). Files in
* VERSION_BUMP_SKIP (CHANGELOG, logs/, .env.*, node_modules/) are never touched.
* The walker only rewrites occurrences whose version literally equals the pre-bump
* version — narrative references like `(v3.2.9, v3.2.10, …)` are preserved.
* 5. Rebuilds CLI docs, deps, client bundle, deploy manifests, default confs.
* 6. Syncs cron setup-start scripts and builds the changelog.
*
* With `options.dryRun: true`, steps 3–4 print a per-file substitution report and exit
* without writing files or running steps 5–6.
*
* @method build
* @param {string} [newVersion] - The new version string to set. Defaults to current version if not provided.
* @param {{dryRun?: boolean, mongoHost?: string, mongoUser?: string, mongoPassword?: string, valkeyHost?: string}} [options] - Commander options.
* `--dry-run` previews changes without writing files.
* `--mongo-host` overrides `DB_HOST` in the template `.env.example` smoke test.
* `--mongo-user` overrides `DB_USER` in the template `.env.example` smoke test.
* `--mongo-password` overrides `DB_PASSWORD` in the template `.env.example` smoke test.
* `--valkey-host` overrides `VALKEY_HOST` in the template `.env.example` smoke test.
* @memberof UnderpostRelease
*/
async build(newVersion, options = {}) {
const dryRun = !!options.dryRun;
dotenv.config({ path: `./engine-private/conf/dd-cron/.env.production`, override: true });
shellCd(`/home/dd/engine`);
// ── Resolve from/to versions up front. Used by every bump step + downstream commands. ──
const originPackageJson = JSON.parse(fs.readFileSync(`package.json`, 'utf8'));
const { version } = originPackageJson;
if (!newVersion) newVersion = version;
logger.info(`Release build — bumping ${version} → ${newVersion}${dryRun ? ' (dry-run)' : ''}`);
if (!dryRun) {
const templateOk = await buildAndTestTemplate(options);
if (!templateOk) return;
}
// ── Canonical version files: delegate to bumpp (package.json, package-lock.json,
// engine-private/conf/**/package.json). bumpp owns lockfile semantics (root version
// + packages[''].version) so we don't reimplement them. ──
const { versionBump } = await import('bumpp');
const bumppResult = await versionBump({
release: newVersion,
files: [
'package.json',
'package-lock.json',
...(fs.existsSync('./engine-private/conf') ? ['engine-private/conf/**/package.json'] : []),
],
commit: false,
tag: false,
push: false,
confirm: false,
recursive: false,
ignoreScripts: true,
dry: dryRun,
});
logger.info(`bumpp updated ${bumppResult?.files?.length ?? 0} canonical file(s).`);
// ── Auxiliary files: anchored-regex walker over VERSION_BUMP_TARGETS. ──
const report = bumpAuxiliaryFiles(version, newVersion, { dryRun });
printBumpReport(report, { dryRun });
if (dryRun) {
logger.info('Dry-run complete. No files modified. Re-run without --dry-run to apply.');
return { from: version, to: newVersion, files: report.map((r) => r.file), dryRun: true };
}
// ── Downstream regenerations and manifest sync (unchanged from previous flow). ──
shellExec(`node bin/deploy cli-docs ${version} ${newVersion}`);
shellExec(`node bin/deploy update-dependencies`);
shellExec(`node bin/build dd`);
shellExec(`node bin run build-cluster-deployment-manifests`);
shellExec(`node bin new --default-conf --conf-workflow-id template`);
shellExec(`sudo rm -rf ./engine-private/conf/dd-default`);
shellExec(`node bin new --deploy-id dd-default`);
console.log(fs.existsSync(`./engine-private/conf/dd-default`));
shellExec(`sudo rm -rf ./engine-private/conf/dd-default`);
shellExec(`node bin cron --kubeadm --setup-start --git`); // --apply
shellExec(`node bin cmt --changelog-build`);
return { from: version, to: newVersion, files: report.map((r) => r.file), dryRun: false };
},
/**
* Runs the local equivalent of an engine-*.ci.yml GitHub Actions workflow.
*
* Mirrors the CI pipeline locally:
* 1. Loads production environment (for GITHUB_TOKEN / GITHUB_USERNAME).
* 2. Clones pwa-microservices-template and engine-{suffix} (bare) into the parent dir (/home/dd).
* 3. Builds dd-{suffix} development from the engine directory.
* 4. Replaces .git in pwa-microservices-template with the bare-cloned git, then commits and pushes
* to the underpostnet/engine-{suffix} remote repository.
*
* @method ci
* @param {string} deployId - The deploy-id suffix (e.g., "cyberia", "core", "lampp", "test").
* Accepts "cyberia", "dd-cyberia", or "engine-cyberia" — the prefix is stripped automatically.
* @param {string} [message] - Optional commit message. Defaults to the last commit message of pwa-microservices-template.
* @param {object} [options] - Commander options object (unused, reserved for future flags).
* @memberof UnderpostRelease
*/
async ci(deployId, message, options) {
dotenv.config({ path: `./engine-private/conf/dd-cron/.env.production`, override: true });
const suffix = deployId.replace(/^(dd-|engine-)/, '');
const repoName = `engine-${suffix}`;
const buildTarget = `dd-${suffix}`;
const githubOrg = process.env.GITHUB_USERNAME || 'underpostnet';
shellCd('/home/dd');
shellExec(`sudo rm -rf /home/dd/pwa-microservices-template`);
shellExec(`node engine/bin clone ${githubOrg}/pwa-microservices-template`);
// Use the message passed from the caller (engine repo changelog);
// fall back to the engine repo's last commit if not provided.
let commitMsg = message;
if (!commitMsg) {
shellCd('/home/dd/engine');
commitMsg = shellExec(`node bin cmt --changelog-msg --changelog-no-hash`, {
stdout: true,
silent: true,
}).trim();
shellCd('/home/dd');
}
commitMsg = (commitMsg || '').trim() || `Update ${repoName} repository`;
logger.info(`CI push commit message: ${commitMsg}`);
shellExec(`node engine/bin clone --bare ${githubOrg}/${repoName}`);
shellCd('/home/dd/engine');
shellExec(`node bin/build ${buildTarget}`);
shellCd('/home/dd/pwa-microservices-template');
shellExec(`rm -rf ./.git`);
shellExec(`mv ../${repoName}.git ./.git`);
shellExec(`git config --local core.bare false`);
shellExec(`git reset`);
Underpost.repo.initLocalRepo({ path: '/home/dd/pwa-microservices-template' });
return {
triggerCmd: `cd /home/dd/pwa-microservices-template && git add . && git commit -m "${commitMsg}" && node ../engine/bin push . ${githubOrg}/${repoName}`,
};
},
/**
* Runs the pwa-microservices-template update and push flow locally.
*
* Always removes and re-clones pwa-microservices-template, then:
* 1. Runs build:template (node bin/build.template) to sync engine sources.
* 2. Installs dependencies and builds the template.
* 3. Commits and pushes to the pwa-microservices-template remote repository.
*
* @method pwa
* @param {string} [message] - Optional commit message. Defaults to last commit message of pwa-microservices-template.
* @param {object} [options] - Commander options object (unused, reserved for future flags).
* @memberof UnderpostRelease
*/
async pwa(message, options) {
dotenv.config({ path: `./engine-private/conf/dd-cron/.env.production`, override: true });
const githubOrg = process.env.GITHUB_USERNAME || 'underpostnet';
// Use the message passed from the caller (engine repo changelog);
// fall back to the engine repo's last commit if not provided.
let commitMsg = message;
if (!commitMsg) {
shellCd('/home/dd/engine');
commitMsg = shellExec(`node bin cmt --changelog-msg --changelog-no-hash`, {
stdout: true,
silent: true,
}).trim();
}
commitMsg = (commitMsg || '').trim() || `Update pwa-microservices-template repository`;
shellCd('/home/dd');
shellExec(`sudo rm -rf /home/dd/pwa-microservices-template`);
shellExec(`node engine/bin clone ${githubOrg}/pwa-microservices-template`);
shellCd('/home/dd/engine');
shellExec(`npm run build:template`);
shellExec(`cd ../pwa-microservices-template && npm install && npm run build`);
shellCd('/home/dd/pwa-microservices-template');
shellExec(`git add .`);
// shellExec(`git commit -m "${commitMsg}"`);
return {
triggerCmd: `node bin push . ${githubOrg}/engine && cd /home/dd/pwa-microservices-template && git commit -m "${commitMsg}" && node ../engine/bin push . ${githubOrg}/pwa-microservices-template`,
};
},
/**
* Deploys a new version release to remote repositories.
*
* Performs the release deployment pipeline:
* 1. Loads production environment from dd-cron.
* 2. Syncs Underpost secrets from the production env file.
* 3. Builds the dd configuration.
* 4. Stages all changes in both engine and engine-private repositories.
* 5. Commits with a release message including the version tag.
* 6. Pushes both repositories to their respective GitHub remotes.
*
* @method deploy
* @param {string} [version] - The version string for the release commit message (e.g., "3.1.4").
* @param {object} [options] - Commander options object (unused, reserved for future flags).
* @memberof UnderpostRelease
*/
async deploy(version, options) {
dotenv.config({ path: `./engine-private/conf/dd-cron/.env.production`, override: true });
killDevServers();
shellExec(
`node bin secret underpost --create-from-file /home/dd/engine/engine-private/conf/dd-cron/.env.production`,
);
shellExec(`node bin/build dd --conf`);
shellExec(`git add . && cd ./engine-private && git add .`);
shellExec(`node bin cmt . ci package-pwa-microservices-template 'New release v:${version}'`);
shellExec(`node bin cmt ./engine-private ci package-pwa-microservices-template`);
shellExec(`node bin push . ${process.env.GITHUB_USERNAME}/engine`);
shellExec(`cd ./engine-private && node ../bin push . ${process.env.GITHUB_USERNAME}/engine-private`);
},
};
}
export { UnderpostRelease };
export default UnderpostRelease;