underpost 2.99.6 → 2.99.7

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.
@@ -773,13 +773,13 @@ rm -rf ${artifacts.join(' ')}`);
773
773
  bootstrapArch,
774
774
  callbackMetaData,
775
775
  steps: [
776
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].base(),
777
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].user(),
778
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].timezone({
776
+ ...Underpost.system.factory[systemProvisioning].base(),
777
+ ...Underpost.system.factory[systemProvisioning].user(),
778
+ ...Underpost.system.factory[systemProvisioning].timezone({
779
779
  timezone,
780
780
  chronyConfPath,
781
781
  }),
782
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].keyboard(keyboard.layout),
782
+ ...Underpost.system.factory[systemProvisioning].keyboard(keyboard.layout),
783
783
  ],
784
784
  });
785
785
  }
@@ -826,13 +826,13 @@ rm -rf ${artifacts.join(' ')}`);
826
826
  bootstrapArch,
827
827
  callbackMetaData,
828
828
  steps: [
829
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].base(),
830
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].user(),
831
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].timezone({
829
+ ...Underpost.system.factory[systemProvisioning].base(),
830
+ ...Underpost.system.factory[systemProvisioning].user(),
831
+ ...Underpost.system.factory[systemProvisioning].timezone({
832
832
  timezone,
833
833
  chronyConfPath: chronyc.chronyConfPath,
834
834
  }),
835
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].keyboard(keyboard.layout),
835
+ ...Underpost.system.factory[systemProvisioning].keyboard(keyboard.layout),
836
836
  ],
837
837
  });
838
838
  }
@@ -2961,320 +2961,6 @@ EOF`);
2961
2961
  throw new Error(`Unsupported host architecture: ${machine}`);
2962
2962
  },
2963
2963
 
2964
- /**
2965
- * @property {object} systemProvisioningFactory
2966
- * @description A factory object containing functions for system provisioning based on OS type.
2967
- * Each OS type (e.g., 'ubuntu') provides methods for base system setup, user creation,
2968
- * timezone configuration, and keyboard layout settings. *
2969
- * @memberof UnderpostBaremetal
2970
- * @namespace UnderpostBaremetal.systemProvisioningFactory
2971
- */
2972
- systemProvisioningFactory: {
2973
- /**
2974
- * @property {object} ubuntu
2975
- * @description Provisioning steps for Ubuntu-based systems.
2976
- * @memberof UnderpostBaremetal.systemProvisioningFactory
2977
- * @namespace UnderpostBaremetal.systemProvisioningFactory.ubuntu
2978
- */
2979
- ubuntu: {
2980
- /**
2981
- * @method base
2982
- * @description Generates shell commands for basic Ubuntu system provisioning.
2983
- * This includes updating package lists, installing essential build tools,
2984
- * kernel modules, cloud-init, SSH server, and other core utilities.
2985
- * @param {object} params - The parameters for the function.
2986
- * @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
2987
- * @returns {string[]} An array of shell commands.
2988
- */
2989
- base: () => [
2990
- // Configure APT sources for Ubuntu ports
2991
- `cat <<SOURCES | tee /etc/apt/sources.list
2992
- deb http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse
2993
- deb http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse
2994
- deb http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse
2995
- SOURCES`,
2996
-
2997
- // Update package lists and perform a full system upgrade
2998
- `apt update -qq`,
2999
- `apt -y full-upgrade`,
3000
-
3001
- // Install all essential packages in one consolidated step
3002
- `DEBIAN_FRONTEND=noninteractive apt install -y build-essential xinput x11-xkb-utils usbutils uuid-runtime linux-image-generic systemd-sysv openssh-server sudo locales udev util-linux iproute2 netplan.io ca-certificates curl wget chrony apt-utils tzdata kmod keyboard-configuration console-setup iputils-ping`,
3003
-
3004
- // Ensure systemd is the init system
3005
- `ln -sf /lib/systemd/systemd /sbin/init`,
3006
-
3007
- // Clean up
3008
- `apt-get clean`,
3009
- ],
3010
- /**
3011
- * @method user
3012
- * @description Generates shell commands for creating a root user and configuring SSH access.
3013
- * This is a critical security step for initial access to the provisioned system.
3014
- * @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
3015
- * @returns {string[]} An array of shell commands.
3016
- */
3017
- user: () => [
3018
- `useradd -m -s /bin/bash -G sudo root`, // Create a root user with bash shell and sudo privileges.
3019
- `echo 'root:root' | chpasswd`, // Set a default password for the root user (consider more secure methods for production).
3020
- `mkdir -p /home/root/.ssh`, // Create .ssh directory for authorized keys.
3021
- // Add the public SSH key to authorized_keys for passwordless login.
3022
- `echo '${fs.readFileSync(
3023
- `/home/dd/engine/engine-private/deploy/id_rsa.pub`,
3024
- 'utf8',
3025
- )}' > /home/root/.ssh/authorized_keys`,
3026
- `chown -R root /home/root/.ssh`, // Set ownership for security.
3027
- `chmod 700 /home/root/.ssh`, // Set permissions for the .ssh directory.
3028
- `chmod 600 /home/root/.ssh/authorized_keys`, // Set permissions for authorized_keys.
3029
- ],
3030
- /**
3031
- * @method timezone
3032
- * @description Generates shell commands for configuring the system timezone and Chrony (NTP client).
3033
- * Accurate time synchronization is essential for logging, security, and distributed systems.
3034
- * @param {object} params - The parameters for the function.
3035
- * @param {string} params.timezone - The timezone string (e.g., 'America/New_York').
3036
- * @param {string} params.chronyConfPath - The path to the Chrony configuration file.
3037
- * @param {string} [alias='chrony'] - The alias for the chrony service.
3038
- * @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
3039
- * @returns {string[]} An array of shell commands.
3040
- */
3041
- timezone: ({ timezone, chronyConfPath }, alias = 'chrony') => [
3042
- `export DEBIAN_FRONTEND=noninteractive`, // Set non-interactive mode for Debian packages.
3043
- `ln -fs /usr/share/zoneinfo/${timezone} /etc/localtime`, // Symlink timezone.
3044
- `sudo dpkg-reconfigure --frontend noninteractive tzdata`, // Reconfigure timezone data.
3045
- `sudo timedatectl set-timezone ${timezone}`, // Set timezone using timedatectl.
3046
- `sudo timedatectl set-ntp true`, // Enable NTP synchronization.
3047
-
3048
- // Write the Chrony configuration file.
3049
- `echo '
3050
- # Use public servers from the pool.ntp.org project.
3051
- # Please consider joining the pool (http://www.pool.ntp.org/join.html).
3052
- # pool 2.pool.ntp.org iburst
3053
- server ${process.env.MAAS_NTP_SERVER} iburst
3054
-
3055
- # Record the rate at which the system clock gains/losses time.
3056
- driftfile /var/lib/chrony/drift
3057
-
3058
- # Allow the system clock to be stepped in the first three updates
3059
- # if its offset is larger than 1 second.
3060
- makestep 1.0 3
3061
-
3062
- # Enable kernel synchronization of the real-time clock (RTC).
3063
- rtcsync
3064
-
3065
- # Enable hardware timestamping on all interfaces that support it.
3066
- #hwtimestamp *
3067
-
3068
- # Increase the minimum number of selectable sources required to adjust
3069
- # the system clock.
3070
- #minsources 2
3071
-
3072
- # Allow NTP client access from local network.
3073
- #allow 192.168.0.0/16
3074
-
3075
- # Serve time even if not synchronized to a time source.
3076
- #local stratum 10
3077
-
3078
- # Specify file containing keys for NTP authentication.
3079
- keyfile /etc/chrony.keys
3080
-
3081
- # Get TAI-UTC offset and leap seconds from the system tz database.
3082
- leapsectz right/UTC
3083
-
3084
- # Specify directory for log files.
3085
- logdir /var/log/chrony
3086
-
3087
- # Select which information is logged.
3088
- #log measurements statistics tracking
3089
- ' > ${chronyConfPath}`,
3090
- `systemctl stop ${alias}`, // Stop Chrony service before reconfiguring.
3091
-
3092
- // Enable, restart, and check status of Chrony service.
3093
- `sudo systemctl enable --now ${alias}`,
3094
- `sudo systemctl restart ${alias}`,
3095
-
3096
- // Wait for chrony to synchronize
3097
- `echo "Waiting for chrony to synchronize..."`,
3098
- `for i in {1..30}; do chronyc tracking | grep -q "Leap status : Normal" && break || sleep 2; done`,
3099
-
3100
- `sudo systemctl status ${alias}`,
3101
-
3102
- // Verify Chrony synchronization.
3103
- `chronyc sources`,
3104
- `chronyc tracking`,
3105
-
3106
- `chronyc sourcestats -v`, // Display source statistics.
3107
- `timedatectl status`, // Display current time and date settings.
3108
- ],
3109
- /**
3110
- * @method keyboard
3111
- * @description Generates shell commands for configuring the keyboard layout.
3112
- * This ensures correct input behavior on the provisioned system.
3113
- * @param {string} [keyCode='en'] - The keyboard layout code (e.g., 'en', 'es').
3114
- * @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
3115
- * @returns {string[]} An array of shell commands.
3116
- */
3117
- keyboard: (keyCode = 'en') => [
3118
- `sudo locale-gen en_US.UTF-8`,
3119
- `sudo update-locale LANG=en_US.UTF-8`,
3120
- `sudo sed -i 's/XKBLAYOUT="us"/XKBLAYOUT="${keyCode}"/' /etc/default/keyboard`,
3121
- `sudo dpkg-reconfigure --frontend noninteractive keyboard-configuration`,
3122
- `sudo systemctl restart keyboard-setup.service`,
3123
- ],
3124
- },
3125
- /**
3126
- * @property {object} rocky
3127
- * @description Provisioning steps for Rocky Linux-based systems.
3128
- * @memberof UnderpostBaremetal.systemProvisioningFactory
3129
- * @namespace UnderpostBaremetal.systemProvisioningFactory.rocky
3130
- */
3131
- rocky: {
3132
- /**
3133
- * @method base
3134
- * @description Generates shell commands for basic Rocky Linux system provisioning.
3135
- * This includes installing Node.js, npm, and underpost CLI tools.
3136
- * @param {object} params - The parameters for the function.
3137
- * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
3138
- * @returns {string[]} An array of shell commands.
3139
- */
3140
- base: () => [
3141
- // Update system and install EPEL repository
3142
- `dnf -y update`,
3143
- `dnf -y install epel-release`,
3144
-
3145
- // Install essential system tools (avoiding duplicates from container packages)
3146
- `dnf -y install --allowerasing bzip2 openssh-server nano vim-enhanced less openssl-devel git gnupg2 libnsl perl`,
3147
- `dnf clean all`,
3148
-
3149
- // Install Node.js
3150
- `curl -fsSL https://rpm.nodesource.com/setup_24.x | bash -`,
3151
- `dnf install -y nodejs`,
3152
- `dnf clean all`,
3153
-
3154
- // Verify Node.js and npm versions
3155
- `node --version`,
3156
- `npm --version`,
3157
-
3158
- // Install underpost ci/cd cli
3159
- `npm install -g underpost`,
3160
- `underpost --version`,
3161
- ],
3162
- /**
3163
- * @method user
3164
- * @description Generates shell commands for creating a root user and configuring SSH access on Rocky Linux.
3165
- * This is a critical security step for initial access to the provisioned system.
3166
- * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
3167
- * @returns {string[]} An array of shell commands.
3168
- */
3169
- user: () => [
3170
- `useradd -m -s /bin/bash -G wheel root`, // Create a root user with bash shell and wheel group (sudo on RHEL)
3171
- `echo 'root:root' | chpasswd`, // Set a default password for the root user
3172
- `mkdir -p /home/root/.ssh`, // Create .ssh directory for authorized keys
3173
- // Add the public SSH key to authorized_keys for passwordless login
3174
- `echo '${fs.readFileSync(
3175
- `/home/dd/engine/engine-private/deploy/id_rsa.pub`,
3176
- 'utf8',
3177
- )}' > /home/root/.ssh/authorized_keys`,
3178
- `chown -R root:root /home/root/.ssh`, // Set ownership for security
3179
- `chmod 700 /home/root/.ssh`, // Set permissions for the .ssh directory
3180
- `chmod 600 /home/root/.ssh/authorized_keys`, // Set permissions for authorized_keys
3181
- ],
3182
- /**
3183
- * @method timezone
3184
- * @description Generates shell commands for configuring the system timezone on Rocky Linux.
3185
- * @param {object} params - The parameters for the function.
3186
- * @param {string} params.timezone - The timezone string (e.g., 'America/Santiago').
3187
- * @param {string} params.chronyConfPath - The path to the Chrony configuration file (optional).
3188
- * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
3189
- * @returns {string[]} An array of shell commands.
3190
- */
3191
- timezone: ({ timezone, chronyConfPath = '/etc/chrony.conf' }) => [
3192
- // Set system timezone using both methods (for chroot and running system)
3193
- `ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`,
3194
- `echo '${timezone}' > /etc/timezone`,
3195
- `timedatectl set-timezone ${timezone} 2>/dev/null`,
3196
-
3197
- // Configure chrony with local NTP server and common NTP pools
3198
- `echo '# Local NTP server' > ${chronyConfPath}`,
3199
- `echo 'server 192.168.1.1 iburst prefer' >> ${chronyConfPath}`,
3200
- `echo '' >> ${chronyConfPath}`,
3201
- `echo '# Fallback public NTP servers' >> ${chronyConfPath}`,
3202
- `echo 'server 0.pool.ntp.org iburst' >> ${chronyConfPath}`,
3203
- `echo 'server 1.pool.ntp.org iburst' >> ${chronyConfPath}`,
3204
- `echo 'server 2.pool.ntp.org iburst' >> ${chronyConfPath}`,
3205
- `echo 'server 3.pool.ntp.org iburst' >> ${chronyConfPath}`,
3206
- `echo '' >> ${chronyConfPath}`,
3207
- `echo '# Configuration' >> ${chronyConfPath}`,
3208
- `echo 'driftfile /var/lib/chrony/drift' >> ${chronyConfPath}`,
3209
- `echo 'makestep 1.0 3' >> ${chronyConfPath}`,
3210
- `echo 'rtcsync' >> ${chronyConfPath}`,
3211
- `echo 'logdir /var/log/chrony' >> ${chronyConfPath}`,
3212
-
3213
- // Enable chronyd to start on boot
3214
- `systemctl enable chronyd 2>/dev/null`,
3215
-
3216
- // Create systemd link for boot (works in chroot)
3217
- `mkdir -p /etc/systemd/system/multi-user.target.wants`,
3218
- `ln -sf /usr/lib/systemd/system/chronyd.service /etc/systemd/system/multi-user.target.wants/chronyd.service 2>/dev/null`,
3219
-
3220
- // Start chronyd if systemd is running
3221
- `systemctl start chronyd 2>/dev/null`,
3222
-
3223
- // Restart chronyd to apply configuration
3224
- `systemctl restart chronyd 2>/dev/null`,
3225
-
3226
- // Force immediate time synchronization (only if chronyd is running)
3227
- `chronyc makestep 2>/dev/null`,
3228
-
3229
- // Verify timezone configuration
3230
- `ls -l /etc/localtime`,
3231
- `cat /etc/timezone || echo 'No /etc/timezone file'`,
3232
- `timedatectl status 2>/dev/null || echo 'Timezone set to ${timezone} (timedatectl not available in chroot)'`,
3233
- `chronyc tracking 2>/dev/null || echo 'Chrony configured but not running (will start on boot)'`,
3234
- ],
3235
- /**
3236
- * @method keyboard
3237
- * @description Generates shell commands for configuring the keyboard layout on Rocky Linux.
3238
- * This uses localectl to set the keyboard layout for both console and X11.
3239
- * @param {string} [keyCode='us'] - The keyboard layout code (e.g., 'us', 'es').
3240
- * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
3241
- * @returns {string[]} An array of shell commands.
3242
- */
3243
- keyboard: (keyCode = 'us') => [
3244
- // Configure vconsole.conf for console keyboard layout (persistent)
3245
- `echo 'KEYMAP=${keyCode}' > /etc/vconsole.conf`,
3246
- `echo 'FONT=latarcyrheb-sun16' >> /etc/vconsole.conf`,
3247
-
3248
- // Configure locale.conf for system locale
3249
- `echo 'LANG=en_US.UTF-8' > /etc/locale.conf`,
3250
- `echo 'LC_ALL=en_US.UTF-8' >> /etc/locale.conf`,
3251
-
3252
- // Set keyboard layout using localectl (works if systemd is running)
3253
- `localectl set-locale LANG=en_US.UTF-8 2>/dev/null`,
3254
- `localectl set-keymap ${keyCode} 2>/dev/null`,
3255
- `localectl set-x11-keymap ${keyCode} 2>/dev/null`,
3256
-
3257
- // Configure X11 keyboard layout file directly
3258
- `mkdir -p /etc/X11/xorg.conf.d`,
3259
- `echo 'Section "InputClass"' > /etc/X11/xorg.conf.d/00-keyboard.conf`,
3260
- `echo ' Identifier "system-keyboard"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
3261
- `echo ' MatchIsKeyboard "on"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
3262
- `echo ' Option "XkbLayout" "${keyCode}"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
3263
- `echo 'EndSection' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
3264
-
3265
- // Load the keymap immediately (if not in chroot)
3266
- `loadkeys ${keyCode} 2>/dev/null || echo 'Keymap ${keyCode} configured (loadkeys not available in chroot)'`,
3267
-
3268
- // Verify configuration
3269
- `echo 'Keyboard configuration files:'`,
3270
- `cat /etc/vconsole.conf`,
3271
- `cat /etc/locale.conf`,
3272
- `cat /etc/X11/xorg.conf.d/00-keyboard.conf 2>/dev/null || echo 'X11 config created'`,
3273
- `localectl status 2>/dev/null || echo 'Keyboard layout set to ${keyCode} (localectl not available in chroot)'`,
3274
- ],
3275
- },
3276
- },
3277
-
3278
2964
  /**
3279
2965
  * @method rebuildNfsServer
3280
2966
  * @description Configures and restarts the NFS server to export the specified path.
@@ -64,7 +64,7 @@ class UnderpostCloudInit {
64
64
  fs.writeFileSync(
65
65
  `${nfsHostToolsPath}/date.sh`,
66
66
  Underpost.baremetal.stepsRender(
67
- Underpost.baremetal.systemProvisioningFactory[systemProvisioning].timezone({
67
+ Underpost.system.factory[systemProvisioning].timezone({
68
68
  timezone,
69
69
  chronyConfPath,
70
70
  }),
@@ -78,7 +78,7 @@ class UnderpostCloudInit {
78
78
  fs.writeFileSync(
79
79
  `${nfsHostToolsPath}/keyboard.sh`,
80
80
  Underpost.baremetal.stepsRender(
81
- Underpost.baremetal.systemProvisioningFactory[systemProvisioning].keyboard(keyboard.layout),
81
+ Underpost.system.factory[systemProvisioning].keyboard(keyboard.layout),
82
82
  false,
83
83
  ),
84
84
  'utf8',
package/src/cli/index.js CHANGED
@@ -76,11 +76,20 @@ program
76
76
  .option('--info', 'Displays information about available commit types.')
77
77
  .option('--diff', 'Shows the current git diff changes.')
78
78
  .option('--edit', 'Edit last commit.')
79
- .option('--msg <msg>', 'Sets a custom commit message.')
80
79
  .option('--deploy-id <deploy-id>', 'Sets the deployment configuration ID for the commit context.')
81
80
  .option('--cached', 'Commit staged changes only or context.')
82
81
  .option('--hashes <hashes>', 'Comma-separated list of specific file hashes of commits.')
83
82
  .option('--extension <extension>', 'specific file extensions of commits.')
83
+ .option(
84
+ '--changelog [latest-n]',
85
+ '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',
86
+ )
87
+ .option('--changelog-build', 'Builds a CHANGELOG.md file based on the commit history')
88
+ .option('--changelog-min-version <version>', 'Sets the minimum version limit for --changelog-build (default: 2.85.0)')
89
+ .option(
90
+ '--changelog-no-hash',
91
+ 'Excludes commit hashes from the generated changelog entries (used with --changelog-build).',
92
+ )
84
93
  .description('Manages commits to a GitHub repository, supporting various commit types and options.')
85
94
  .action(Underpost.repo.commit);
86
95
 
@@ -426,6 +435,7 @@ program
426
435
  '--create-job-now',
427
436
  'After applying manifests, immediately create a Job from each CronJob (requires --apply).',
428
437
  )
438
+ .option('--ssh', 'Execute backup commands via SSH on the remote node instead of locally.')
429
439
  .description('Manages cron jobs: execute jobs directly or generate and apply K8s CronJob manifests.')
430
440
  .action(Underpost.cron.callback);
431
441
 
@@ -502,6 +512,7 @@ program
502
512
  .option('--status', 'Checks the status of the SSH service.')
503
513
  .option('--connect-uri', 'Displays the connection URI.')
504
514
  .option('--copy', 'Copies the connection URI to clipboard.')
515
+ .description('Manages SSH credentials and sessions for remote access to cluster nodes or services.')
505
516
  .action(Underpost.ssh.callback);
506
517
 
507
518
  program
@@ -88,10 +88,13 @@ class UnderpostRepository {
88
88
  * @param {boolean} [options.cached=false] - If true, commits only staged changes.
89
89
  * @param {number} [options.log=0] - If greater than 0, shows the last N commits with diffs.
90
90
  * @param {boolean} [options.lastMsg=0] - If greater than 0, copies or show the last last single n commit message to clipboard.
91
- * @param {string} [options.msg=''] - If provided, outputs this message instead of committing.
92
91
  * @param {string} [options.deployId=''] - An optional deploy ID to include in the commit message.
93
92
  * @param {string} [options.hashes=''] - If provided with diff option, shows the diff between two hashes.
94
93
  * @param {string} [options.extension=''] - If provided with diff option, filters the diff by this file extension.
94
+ * @param {boolean|string} [options.changelog=undefined] - If true, prints the changelog since the last CI integration commit (starting with 'ci(package-pwa-microservices-'). If a number string, prints the changelog of the last N commits split by version sections. Only considers commits starting with '[<tag>]'.
95
+ * @param {boolean} [options.changelogBuild=false] - If true, scrapes all git history and builds a full CHANGELOG.md. Commits containing 'New release v:' are used as version section titles. Only commits starting with '[<tag>]' are included as entries.
96
+ * @param {string} [options.changelogMinVersion=''] - If set, overrides the default minimum version limit (2.85.0) for --changelog-build.
97
+ * @param {boolean} [options.changelogNoHash=false] - If true, omits commit hashes from the changelog entries.
95
98
  * @memberof UnderpostRepository
96
99
  */
97
100
  commit(
@@ -108,13 +111,169 @@ class UnderpostRepository {
108
111
  cached: false,
109
112
  lastMsg: 0,
110
113
  log: 0,
111
- msg: '',
112
114
  deployId: '',
113
115
  hashes: '',
114
116
  extension: '',
117
+ changelog: undefined,
118
+ changelogBuild: false,
119
+ changelogMinVersion: '',
120
+ changelogNoHash: false,
115
121
  },
116
122
  ) {
117
123
  if (!repoPath) repoPath = '.';
124
+
125
+ if (options.changelog !== undefined || options.changelogBuild) {
126
+ const ciIntegrationPrefix = 'ci(package-pwa-microservices-';
127
+ const releaseMatch = 'New release v:';
128
+
129
+ // Helper: parse [<tag>] commits into grouped sections
130
+ const buildSectionChangelog = (commits) => {
131
+ const groups = {};
132
+ const tagOrder = [];
133
+ for (const commit of commits) {
134
+ if (!commit.message.startsWith('[')) continue;
135
+ const match = commit.message.match(/^\[([^\]]+)\]\s*(.*)/);
136
+ if (match) {
137
+ const tag = match[1].trim();
138
+ const context = match[2].trim().replaceAll('"', '');
139
+ if (!groups[tag]) {
140
+ groups[tag] = [];
141
+ tagOrder.push(tag);
142
+ }
143
+ groups[tag].push({ ...commit, context });
144
+ }
145
+ }
146
+ let out = '';
147
+ for (const tag of tagOrder) {
148
+ out += `### ${tag}\n\n`;
149
+ for (const entry of groups[tag]) {
150
+ out += `- ${entry.context}${options.changelogNoHash ? '' : ` (${commitUrl(entry.hash, entry.fullHash)})`}\n`;
151
+ }
152
+ out += '\n';
153
+ }
154
+ return out;
155
+ };
156
+
157
+ // Helper: fetch git log as structured array
158
+ const fetchHistory = (limit) => {
159
+ const limitArg = limit ? ` -n ${limit}` : '';
160
+ const rawLog = shellExec(`git log --pretty=format:"%h||%H||%s||%ci"${limitArg}`, {
161
+ stdout: true,
162
+ silent: true,
163
+ disableLog: true,
164
+ });
165
+ return rawLog
166
+ .split('\n')
167
+ .map((line) => {
168
+ const parts = line.split('||');
169
+ return {
170
+ hash: (parts[0] || '').trim(),
171
+ fullHash: (parts[1] || '').trim(),
172
+ message: parts[2] || '',
173
+ date: parts[3] || '',
174
+ };
175
+ })
176
+ .filter((c) => c.hash);
177
+ };
178
+
179
+ const githubUser = process.env.GITHUB_USERNAME || 'underpostnet';
180
+ const commitUrl = (shortHash, fullHash) =>
181
+ `[${shortHash}](https://github.com/${githubUser}/engine/commit/${fullHash})`;
182
+
183
+ // Helper: extract version from commit message containing 'New release v:'
184
+ const extractVersion = (message) => {
185
+ const idx = message.indexOf(releaseMatch);
186
+ if (idx === -1) return null;
187
+ return message.substring(idx + releaseMatch.length).trim();
188
+ };
189
+
190
+ // Helper: split commits array into version sections by 'New release v:' boundary
191
+ const buildVersionSections = (commits) => {
192
+ const sections = [];
193
+ let currentSection = { title: null, date: new Date().toISOString().split('T')[0], commits: [] };
194
+
195
+ for (const commit of commits) {
196
+ const version = extractVersion(commit.message);
197
+ if (version) {
198
+ // Push accumulated commits as a section
199
+ sections.push(currentSection);
200
+ // Start new version section; commits below this one belong to it
201
+ const commitDate = commit.date ? commit.date.split(' ')[0] : '';
202
+ currentSection = { title: `${releaseMatch}${version}`, date: commitDate, hash: commit.hash, commits: [] };
203
+ } else {
204
+ currentSection.commits.push(commit);
205
+ }
206
+ }
207
+ // Push the last (oldest) section
208
+ if (currentSection.commits.length > 0) sections.push(currentSection);
209
+ return sections;
210
+ };
211
+
212
+ // Helper: render sections array into changelog markdown string
213
+ const renderSections = (sections) => {
214
+ let changelog = '';
215
+ for (const section of sections) {
216
+ const sectionBody = buildSectionChangelog(section.commits);
217
+ if (!sectionBody) continue;
218
+ if (section.title) {
219
+ changelog += `## ${section.title}${options.changelogNoHash ? '' : ` (${section.date})`}\n\n`;
220
+ } else {
221
+ changelog += `## ${section.date}\n\n`;
222
+ }
223
+ changelog += sectionBody;
224
+ }
225
+ return changelog;
226
+ };
227
+
228
+ const changelogMinVersion = options.changelogMinVersion || '2.97.1';
229
+
230
+ if (options.changelogBuild) {
231
+ // --changelog-build: scrape ALL history, split by 'New release v:' commits as version sections
232
+ const allCommits = fetchHistory();
233
+ const sections = buildVersionSections(allCommits);
234
+
235
+ // Filter sections: stop at changelogMinVersion boundary
236
+ const limitedSections = [];
237
+ for (const section of sections) {
238
+ limitedSections.push(section);
239
+ if (section.title) {
240
+ const versionStr = section.title.replace(releaseMatch, '').trim();
241
+ if (versionStr === changelogMinVersion) break;
242
+ }
243
+ }
244
+
245
+ let changelog = renderSections(limitedSections);
246
+
247
+ if (!changelog) {
248
+ changelog = `No changelog entries found.\n`;
249
+ }
250
+
251
+ const changelogPath = `${repoPath === '.' ? '.' : repoPath}/CHANGELOG.md`;
252
+ fs.writeFileSync(changelogPath, `# Changelog\n\n${changelog}`);
253
+ logger.info('CHANGELOG.md built at', changelogPath);
254
+ } else {
255
+ // --changelog [latest-n]: print changelog of last N commits or since last release
256
+ const hasExplicitCount =
257
+ options.changelog !== undefined && options.changelog !== true && !isNaN(parseInt(options.changelog));
258
+ const scanLimit = hasExplicitCount ? parseInt(options.changelog) : 500;
259
+ const allCommits = fetchHistory(scanLimit);
260
+
261
+ let commits;
262
+ if (!hasExplicitCount) {
263
+ // No explicit count: find commits up to the last CI integration boundary
264
+ const ciIndex = allCommits.findIndex((c) => c.message.startsWith(ciIntegrationPrefix));
265
+ commits = ciIndex >= 0 ? allCommits.slice(0, ciIndex) : allCommits;
266
+ } else {
267
+ commits = allCommits;
268
+ }
269
+
270
+ const sections = buildVersionSections(commits);
271
+ let changelog = renderSections(sections);
272
+ console.log(changelog || `No changelog entries found.\n`);
273
+ }
274
+
275
+ return;
276
+ }
118
277
  if (options.diff && options.hashes) {
119
278
  const hashes = options.hashes.split(',');
120
279
  const cmd = `git --no-pager diff ${hashes[0]} ${hashes[1] ? hashes[1] : 'HEAD'}${options.extension ? ` -- '*.${options.extension}'` : ''}`;
@@ -123,16 +282,6 @@ class UnderpostRepository {
123
282
  } else console.log(cmd);
124
283
  return;
125
284
  }
126
- if (options.msg) {
127
- options.msg = options.msg.replaceAll('"', '').replaceAll(`'`, '').replaceAll('`', '');
128
- let key = Object.keys(commitData).find((k) => k && options.msg.toLocaleLowerCase().slice(0, 16).match(k));
129
- if (!key) key = Object.keys(commitData).find((k) => k && options.msg.toLocaleLowerCase().match(k));
130
- if (!key || key === undefined) key = 'chore';
131
- shellExec(
132
- `underpost cmt ${repoPath} ${key} ${options.deployId ? options.deployId : `''`} '${options.msg.replaceAll(`${key}(${key}`, '')}'`,
133
- );
134
- return;
135
- }
136
285
  if (options.lastMsg) {
137
286
  if (options.copy) {
138
287
  pbcopy(Underpost.repo.getLastCommitMsg(options.lastMsg - 1));
@@ -197,7 +346,11 @@ class UnderpostRepository {
197
346
  * @memberof UnderpostRepository
198
347
  */
199
348
  getLastCommitMsg(skip = 0) {
200
- return shellExec(`git --no-pager log -1 --skip=${skip} --pretty=%B`, { stdout: true });
349
+ return shellExec(`git --no-pager log -1 --skip=${skip} --pretty=%B`, {
350
+ stdout: true,
351
+ silent: true,
352
+ disableLog: true,
353
+ });
201
354
  },
202
355
 
203
356
  /**