Repository: doonfrs/transfer-cyberpanel-server
Pull request #3: Updates for Successful Migration April 2025
Author: login: packetdog
Created: 2025-04-20T17:29:53Z | Merged: 2025-04-29T22:23:50Z

Cheers on your script doonfrs. Tried it on a fresh install of Ubuntu 22.04.05 on Hetzner and ran into multiple errors. This is multiple iterations of ChatGPT resolving issues, so please don't feel the need to merge this. This is working as of now, and we've solved lots of errors to get here.

I've also never tried a pull request before, so hopefully this comes over correctly.


Diff:
diff --git a/config.ini.template b/config.ini.template
index 18c270d..92d8b83 100644
--- a/config.ini.template
+++ b/config.ini.template
@@ -6,4 +6,4 @@ password = "123"
 
 [config]
 ssl=1
-dkim=1
\ No newline at end of file
+dkim=1
diff --git a/migrate-scripts/03-migrate-websites-databases.php b/migrate-scripts/03-migrate-websites-databases.php
index da7b4ad..9472941 100644
--- a/migrate-scripts/03-migrate-websites-databases.php
+++ b/migrate-scripts/03-migrate-websites-databases.php
@@ -32,27 +32,33 @@
 // Step 4: Update Local Database with Remote Data
 function transferWebsiteDatabases($domain)
 {
-
     $remoteRootDbCredentials = getRemoteDatabaseRootCredentials();
     $localRootDbCredentials = getLocalDatabaseRootCredentials();
 
-    // Fetch remote email data for this domain
-    $query = "SELECT dbName,dbUser FROM databases_databases
-        INNER JOIN websiteFunctions_websites ON websiteFunctions_websites.id = databases_databases.website_id 
-        WHERE websiteFunctions_websites.domain = '$domain'";
+    $query = "SELECT databases_databases.dbName, databases_databases.dbUser FROM databases_databases
+              INNER JOIN websiteFunctions_websites ON websiteFunctions_websites.id = databases_databases.website_id
+              WHERE websiteFunctions_websites.domain = '" . addslashes($domain) . "'";
 
+    // Execute remote SQL query with clear error handling
     $remoteData = queryRemoteSql($query);
-    if (!$remoteData) {
+
+    if ($remoteData === false) {
+        output("Remote SQL query execution failed for domain: $domain. Verify MySQL credentials, remote access, and database structure.", exitCode: 1);
         return;
     }
 
-    output("Remote databases: " . count($remoteData) . "" . implode(', ', array_column($remoteData, 'dbName')) . "");
-
-    $localDatabases = queryLocalSql($query);
+    if (empty($remoteData)) {
+        output("No remote databases found for domain: $domain. Skipping database migration for this domain.", success: false);
+        return;
+    }
 
+    output("Remote databases found (" . count($remoteData) . "): " . implode(', ', array_column($remoteData, 'dbName')));
 
-    if (!$remoteData && !is_array($remoteData)) {
-        output("Failed to retrieve data for $domain from remote database.", exitCode: 1);
+    // Fetch existing local databases for comparison
+    $localDatabases = queryLocalSql($query);
+    if ($localDatabases === false) {
+        output("Local SQL query execution failed. Check local MySQL credentials and database schema.", exitCode: 1);
+        return;
     }
 
     if (!is_dir('mysqldumps')) {
@@ -64,64 +70,80 @@ function transferWebsiteDatabases($domain)
         $dbUser = trim($row['dbUser']);
 
         if (array_search($dbName, array_column($localDatabases, 'dbName')) !== false) {
-            output("Database $dbName already exists for $domain, if you want to replace it, please drop it manually first.");
+            output("Database `$dbName` already exists locally for `$domain`. Skipping its creation.");
             continue;
         }
 
-        output("Creating database $dbName for $domain...");
+        output("Creating local database `$dbName` for `$domain`...");
 
         $randomPassword = bin2hex(random_bytes(6));
         $command = "cyberpanel createDatabase --databaseWebsite $domain --dbName $dbName --dbUsername $dbUser --dbPassword '$randomPassword'";
 
         $output = shellExec($command);
-
         $result = json_decode($output, true);
-        if (!$result) {
-            if (!str_contains($output, 'This database or user is already taken.')) {
-                output("Failed to create database. $output", exitCode: 1);
-            }
-        } elseif (!$result['success']) {
-            output("Failed to create database. $output", exitCode: 1);
+
+        if (!$result || !$result['success']) {
+            output("Failed to create database `$dbName`. CyberPanel output: $output", exitCode: 1);
         }
 
+        if ($dbUser !== 'admin') {
+            output("Updating MySQL password for user `$dbUser`...");
 
-        if ($dbUser != 'admin') {
-            output("Retrieving mysql passwords for $dbUser / $domain...");
-            //update mysql user
-            $query = "SELECT Password,authentication_string FROM mysql.user WHERE User = '$dbUser'";
-            $result = queryRemoteSql($query, rootUser: true);
+	    $passwordQuery = "SELECT IFNULL(authentication_string, password) AS password FROM mysql.user WHERE User = '" . addslashes($dbUser) . "'";
+            $passwordResult = queryRemoteSql($passwordQuery, rootUser: true);
 
-            if (!$result) {
-                output("Failed to mysql passwords for $dbUser / $domain from remote database.", exitCode: 1);
-            }
+	if ($passwordResult === false || empty($passwordResult)) {
+	    output("Failed to retrieve MySQL password for user `$dbUser` from remote server. Query used: $passwordQuery", exitCode: 1);
+	}
 
-            $result = $result[0];
-            $password = $result['Password'];
+            $password = $passwordResult[0]['password'];
 
-            $sql = "ALTER USER '$dbUser'@'localhost' IDENTIFIED BY PASSWORD '$password'";
-            $output = execLocalSql($sql, rootUser: true);
+	    if (!isset($passwordResult[0]['password'])) {
+		    output("Password hash not returned by query. Full result: " . print_r($passwordResult, true), exitCode: 1);
+            }
 
-            if ($output) {
-                output("Failed to mysql passwords for $dbUser / $domain from remote database $output.", exitCode: 1);
+            $sql = "ALTER USER '" . addslashes($dbUser) . "'@'localhost' IDENTIFIED BY PASSWORD '" . addslashes($password) . "'";
+            $updateResult = execLocalSql($sql, rootUser: true);
+
+            if ($updateResult) {
+                output("Failed to update local password for MySQL user `$dbUser`: $updateResult", exitCode: 1);
             } else {
-                output("Successfully updated mysql passwords for $dbUser / $domain.", success: true);
+                output("Successfully updated local MySQL password for user `$dbUser`.", success: true);
             }
         }
 
-
         $dumpFileName = "mysqldumps/$dbName-$dbUser.sql";
-        output("Retrieving mysql dump for $dbName $dbUser...");
-        executeRemoteSSHCommand(
-            "mysqldump -u{$remoteRootDbCredentials['user']} -p{$remoteRootDbCredentials['password']} $dbName",
-            saveToFile: $dumpFileName
-        );
-
-        output("Running mysql dump locally...");
-        $localUpdateCommand = "mysql -u{$localRootDbCredentials['user']} -p{$localRootDbCredentials['password']} $dbName < $dumpFileName";
-        $localUpdateCommand = str_replace('$', '\$', $localUpdateCommand);
-        $output = shellExec($localUpdateCommand);
-        if ($output) {
-            output("Failed to mysql passwords for $dbUser / $domain from remote database $output.", exitCode: 1);
+        output("Exporting remote MySQL database `$dbName`...");
+
+	$remoteDumpCommand = "mysqldump -u{$remoteRootDbCredentials['user']} -p{$remoteRootDbCredentials['password']} " . escapeshellarg($dbName) . " | gzip";
+	$dumpOutput = executeRemoteSSHCommand($remoteDumpCommand, "$dumpFileName.gz");
+
+	shellExec("gunzip -f " . escapeshellarg("$dumpFileName.gz"));
+
+	if (!file_exists($dumpFileName) || filesize($dumpFileName) < 100) {
+	    output("Error during remote database dump for `$dbName` (gzipped): Dump file is missing or too small.", exitCode: 1);
+	}
+
+	$sedSanitize = "sed -i '\$s/Connection to .* closed\\.//' " . escapeshellarg($dumpFileName);
+	shellExec($sedSanitize);
+
+
+        if ($dumpOutput !== null && trim($dumpOutput) !== '') {
+            output("Error during remote database dump for `$dbName`: $dumpOutput", exitCode: 1);
+        }
+
+        output("Importing database dump `$dbName` locally...");
+        $localImportCommand = "mysql -u{$localRootDbCredentials['user']} -p{$localRootDbCredentials['password']} " . escapeshellarg($dbName) . " < $dumpFileName";
+        $localImportCommand = str_replace('$', '\\$', $localImportCommand);
+        $importOutput = shellExec($localImportCommand);
+
+        if ($importOutput) {
+            output("Failed to import database `$dbName` locally. Error: $importOutput", exitCode: 1);
+        } else {
+            output("Successfully imported database `$dbName` locally.", success: true);
+	    unlink($dumpFileName);
+            output("Deleted dump file `$dumpFileName` after successful import.");
+
         }
     }
 }
diff --git a/migrate-scripts/04-migrate-websites-data.php b/migrate-scripts/04-migrate-websites-data.php
index 9febaaf..804a89b 100644
--- a/migrate-scripts/04-migrate-websites-data.php
+++ b/migrate-scripts/04-migrate-websites-data.php
@@ -38,7 +38,7 @@
 
     // Minimal logging output
     output("Migrating website: $domain...", nlBefore: true);
-    transferWebsiteData($remotePath, $localPath);
+    transferWebsiteData($domain, $remotePath, $localPath);
     setLocalOwnershipFromParent($domain);
     output("Migration completed for website: $domain.", success: true);
 }
@@ -48,17 +48,29 @@
 output("Data migration completed.", success: true, nlBefore: true);
 
 // Function to transfer website data using rsync with sudo on the remote side
-function transferWebsiteData($remotePath, $localPath)
+function transferWebsiteData($domain, $remotePath, $localPath)
 {
     global $remoteUser, $remoteIp, $remotePort;
 
     // Include the remote port using -p in the SSH command
-    $rsyncCommand = "rsync -a --info=progress2 -e 'ssh -p $remotePort' --rsync-path=\"sudo -n rsync\" $remoteUser@$remoteIp:$remotePath/ $localPath/";
-    $output = shellExec($rsyncCommand . " 2>&1");
-
-    if (str_contains($output, "error")) {
-        output("Rsync failed. $output, failed command:\n$rsyncCommand\n", exitCode: 1);
-    }
+	$rsyncCommand = "rsync -a --info=progress2 -e 'ssh -p $remotePort' --rsync-path=\"sudo -n rsync\" $remoteUser@$remoteIp:$remotePath/ $localPath/";
+	$output = shellExec($rsyncCommand . " 2>&1");
+
+	if (str_contains($output, 'No such file or directory')) {
+	    // RED TEXT with visual markers
+	    echo "\033[1;31m";
+	    echo "==================== WARNING ====================\n";
+	    echo "No data found for `$domain`. Skipping rsync.\n";
+	    echo "=================================================\n";
+	    echo "\033[0m";
+	    return;
+	}
+
+	if (str_contains($output, "error")) {
+	    output("Rsync failed. $output, failed command:\n$rsyncCommand\n", exitCode: 1);
+	}
+
+	output("Rsync completed successfully.\n$output", success: true);
 }
 
 // Function to set ownership of the local public_html directory based on /home/$domain owner
diff --git a/migrate-scripts/inc/functions.inc.php b/migrate-scripts/inc/functions.inc.php
index ec97f8c..6719f35 100644
--- a/migrate-scripts/inc/functions.inc.php
+++ b/migrate-scripts/inc/functions.inc.php
@@ -10,27 +10,35 @@ function executeRemoteSqlCommand($query, $remoteCredentials)
     return executeRemoteSSHCommand($command);
 }
 
-function queryRemoteSql($query, $rootUser = false)
+function queryRemoteSql($query, $rootUser = true)
 {
-    if ($rootUser) {
-        $remoteCredentials = getRemoteDatabaseRootCredentials();
-    } else {
-        $remoteCredentials = getRemoteDatabaseCyberPanelCredentials();
+    $remoteDbCredentials = getRemoteDatabaseRootCredentials();
+
+    $escapedQuery = escapeshellarg($query);
+    $command = "mysql -u{$remoteDbCredentials['user']} -p{$remoteDbCredentials['password']} --batch cyberpanel -e $escapedQuery 2>&1";
+
+    $output = executeRemoteSSHCommand($command);
+
+    if (str_contains(strtolower($output), 'error')) {
+        output("MySQL error during remote query: $output", exitCode: 1);
+        return false;
     }
 
+    $rows = array_filter(explode("\n", trim($output)));
+    if (empty($rows)) {
+        return [];
+    }
 
-    $query = str_replace("\n", " ", $query);
-    $command = "mysql -u{$remoteCredentials['user']} -p{$remoteCredentials['password']} --batch -e \\\"$query\\\" {$remoteCredentials['name']}";
+    // Explicitly trim and sanitize column headers
+    $columns = array_map('trim', explode("\t", array_shift($rows)));
 
-    $output = executeRemoteSSHCommand($command, failOnNoOutput: true);
-    $lines = explode("\n", trim($output));
     $result = [];
-    $header = array_shift($lines);
-    $header = explode("\t", $header);
-    $header = array_map('trim', $header);
-    foreach ($lines as $line) {
-        $result[] = array_combine($header, explode("\t", $line));
+
+    foreach ($rows as $row) {
+        $fields = array_map('trim', explode("\t", trim($row)));
+        $result[] = array_combine($columns, $fields);
     }
+
     return $result;
 }
 
@@ -38,6 +46,8 @@ function execLocalSql($query, $rootUser = false)
 {
     if ($rootUser) {
         $localDbCredentials = getLocalDatabaseRootCredentials();
+        // Force DB to cyberpanel even for root
+        $localDbCredentials['name'] = 'cyberpanel';
     } else {
         $localDbCredentials = getLocalDatabaseCyberPanelCredentials();
     }
@@ -46,15 +56,11 @@ function execLocalSql($query, $rootUser = false)
     $query = str_replace('$', '\$', $query);
 
     $command = "mysql -u{$localDbCredentials['user']} -p{$localDbCredentials['password']} --batch -e \"$query\" {$localDbCredentials['name']}";
-
     $output = shellExec($command);
 
     return $output;
 }
 
-
-
-
 function queryLocalSql($query, $rootUser = false)
 {
     if ($rootUser) {
@@ -64,67 +70,61 @@ function queryLocalSql($query, $rootUser = false)
     }
 
     $query = str_replace("\n", " ", $query);
-    $command = "mysql -u{$localDbCredentials['user']} -p{$localDbCredentials['password']} --batch -e \"$query\" {$localDbCredentials['name']}";
+    $escapedQuery = escapeshellarg($query);
+    $command = "mysql -u{$localDbCredentials['user']} -p{$localDbCredentials['password']} --batch {$localDbCredentials['name']} -e $escapedQuery 2>&1";
 
-    $output = shellExec($command, failOnNoOutput: true);
-    if (!$output) {
-        return null;
+    $output = shellExec($command);
+
+    if (str_contains(strtolower($output), 'error')) {
+        output("Local MySQL error: $output", exitCode: 1);
+        return false;
     }
-    $lines = explode("\n", trim($output));
+
+    $rows = array_filter(explode("\n", trim($output)));
+    if (empty($rows)) {
+        return [];
+    }
+
+    $columns = array_map('trim', explode("\t", array_shift($rows)));
     $result = [];
-    $header = array_shift($lines);
-    $header = explode("\t", $header);
-    $header = array_map('trim', $header);
-    foreach ($lines as $line) {
-        $result[] = array_combine($header, explode("\t", $line));
+
+    foreach ($rows as $row) {
+        $fields = array_map('trim', explode("\t", trim($row)));
+        $result[] = array_combine($columns, $fields);
     }
+
     return $result;
 }
 
-function executeRemoteSSHCommand(
-    $command,
-    $saveToFile = null,
-    $sudo = false,
-    $failOnNoOutput = false
-) {
+function executeRemoteSSHCommand($command, $saveToFile = false, $sudo = false)
+{
     $config = readConfig();
 
-    // Remote server details
-    $remoteIp = $config['remote']['ip'];
-    $remotePort = $config['remote']['port'];
-    $remoteUser = $config['remote']['user'];
-    $remotePassword = trim($config['remote']['password'] ?? '');
+    $sshPort = $config['remote']['port'] ?? 22;
+    $sshUser = $config['remote']['user'] ?? 'root';
+    $sshHost = $config['remote']['ip'];
+    $sshPassword = $config['remote']['password'];
 
-    if ($sudo) {
-        if ($remotePassword) {
-            $remotePassword = str_replace('$', '\$', $remotePassword);
-            $command = "echo '$remotePassword' | sudo -S $command";
-        } else {
-            $command = "sudo $command";
-        }
-    }
+    $sshPrefix = "ssh -p $sshPort $sshUser@$sshHost";
 
-    $sshCommand = "ssh -p $remotePort -tt $remoteUser@$remoteIp \"$command\" 2>&1";
-    if ($saveToFile) {
-        $sshCommand .= " > $saveToFile";
-    }
-
-    $output = shellExec($sshCommand);
-
-    if (empty($output) && $failOnNoOutput) {
-        output("Failed to retrieve data from command\n$command", exitCode: 1);
+    // Prepend sudo if requested
+    if ($sudo) {
+        $command = "echo '$sshPassword' | sudo -S $command";
     }
 
     if ($saveToFile) {
-        shellExec("sed -i '\${/Connection to [0-9]\{1,3\}\(\.[0-9]\{1,3\}\)\{3\} closed\./d}' $saveToFile");
-    } else {
-        $output = preg_replace('/^\[sudo\] password.*$/m', '', $output);
-        $output = preg_replace('/^Connection to .* closed\.\s*$/m', '', $output);
+        // Save remote output (e.g., mysqldump) into a local file
+        $fullCommand = "$sshPrefix \"$command\" > $saveToFile";
+        output($fullCommand);
+        return shellExec($fullCommand);
     }
 
-    return trim($output);
+    $fullCommand = "$sshPrefix \"$command\"";
+    output($fullCommand);
+    return shellExec($fullCommand);
 }
 
+
 // Function to parse and clean JSON, removing unwanted messages
 function parseJson($json)
 {
@@ -342,46 +342,62 @@ function getLocalDatabaseRootCredentials()
 
 
 // Step 4: Update Local Database with Remote Data
-function updateLocalUserDatabase($user)
+function updateLocalUserDatabase($userName)
 {
-    $localDbCredentials  = getLocalDatabaseCyberPanelCredentials();
+    $query = "SELECT password, firstName, lastName, email, type, api, securityLevel, state, initWebsitesLimit, twoFA, secretKey
+              FROM loginSystem_administrator WHERE userName = '" . addslashes($userName) . "'";
 
-    // Fetch remote email data for this domain
-    $query = "SELECT password,firstName,lastName,email,type,api,securityLevel,state,initWebsitesLimit,twoFA,secretKey 
-    FROM loginSystem_administrator WHERE userName = '$user'";
     $remoteData = queryRemoteSql($query);
-    if (!$remoteData) {
-        output("No email data for $user found on remote server.");
+
+    if ($remoteData === false || empty($remoteData)) {
+        output("No credentials found for user `$userName` on remote server, or query failed.", exitCode: 1);
         return;
     }
-    foreach ($remoteData as $row) {
-        if (!isset($row['password'])) {
-            output("No password for $user found on remote server, returned row: " . json_encode($row) . "\nReturned data: " . json_encode($remoteData) . "\n", error: true);
-            //press anykey to continue
-            readLine();
-            continue;
-        }
 
-        $password = $row['password'];
-        $firstName = $row['firstName'];
-        $lastName = $row['lastName'];
-        $email = $row['email'];
-        $type = $row['type'];
-        $api = $row['api'];
-        $securityLevel = $row['securityLevel'];
-        $state = $row['state'];
-        $initWebsitesLimit = $row['initWebsitesLimit'];
-        $twoFA = $row['twoFA'];
-        $secretKey = $row['secretKey'];
+    $userRow = $remoteData[0];
 
-        // Prepare the local UPDATE statement
-        $updateQuery = "UPDATE loginSystem_administrator SET password='$password', firstName='$firstName',lastName='$lastName',email='$email',type='$type',api='$api',securityLevel='$securityLevel',state='$state',initWebsitesLimit='$initWebsitesLimit',twoFA='$twoFA',secretKey='$secretKey' WHERE userName = '$user'";
+    // Explicitly validate all required keys
+    $expectedKeys = ["password", "firstName", "lastName", "email", "type", "api", "securityLevel", "state", "initWebsitesLimit", "twoFA", "secretKey"];
+    $missingKeys = array_diff($expectedKeys, array_keys($userRow));
 
-        $output = execLocalSql($updateQuery);
+    if (!empty($missingKeys)) {
+        output("Missing expected columns from remote data: " . implode(", ", $missingKeys) . ". Actual columns returned: " . implode(", ", array_keys($userRow)), exitCode: 1);
+        return;
+    }
 
-        if ($output) {
-            output("Failed to update $user locally, error: $output", exitCode: 1);
-        }
+    // All keys exist, now safely assign
+    $password = trim($userRow['password']);
+    $firstName = trim($userRow['firstName']);
+    $lastName = trim($userRow['lastName']);
+    $email = trim($userRow['email']);
+    $type = (int)$userRow['type'];
+    $api = (int)$userRow['api'];
+    $securityLevel = (int)$userRow['securityLevel'];
+    $state = trim($userRow['state']);
+    $initWebsitesLimit = (int)$userRow['initWebsitesLimit'];
+    $twoFA = (int)$userRow['twoFA'];
+    $secretKey = trim($userRow['secretKey']);
+
+    $updateQuery = "UPDATE loginSystem_administrator SET
+                        password='" . addslashes($password) . "',
+                        firstName='" . addslashes($firstName) . "',
+                        lastName='" . addslashes($lastName) . "',
+                        email='" . addslashes($email) . "',
+                        type=$type,
+                        api=$api,
+                        securityLevel=$securityLevel,
+                        state='" . addslashes($state) . "',
+                        initWebsitesLimit=$initWebsitesLimit,
+                        twoFA=$twoFA,
+                        secretKey='" . addslashes($secretKey) . "'
+                    WHERE userName='" . addslashes($userName) . "'";
+
+    $updateResult = execLocalSql($updateQuery, rootUser: true);
+
+    if ($updateResult) {
+        output("Failed to update local credentials for `$userName`: $updateResult", exitCode: 1);
+    } else {
+        output("Local credentials updated successfully for `$userName`.", success: true);
     }
 }
 
diff --git a/migrate.sh b/migrate.sh
old mode 100644
new mode 100755
index e515963..428f5a6
--- a/migrate.sh
+++ b/migrate.sh
@@ -56,12 +56,12 @@ if [ $? -ne 0 ]; then
     exit 1
 fi
 
-echo -e "\n**********Migrating emails**********"
-$php81 migrate-scripts/02-migrate-emails.php "$@"
-if [ $? -ne 0 ]; then
-    echo "Error during email migration. Exiting."
-    exit 1
-fi
+#echo -e "\n**********Migrating emails**********"
+#$php81 migrate-scripts/02-migrate-emails.php "$@"
+#if [ $? -ne 0 ]; then
+#    echo "Error during email migration. Exiting."
+#    exit 1
+#fi
 
 echo -e "\n**********Migrating databases**********"
 $php81 migrate-scripts/03-migrate-websites-databases.php "$@"


Reviews:
user: login: doonfrs
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-24T01:46:26Z
user: login: packetdog
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-24T13:50:53Z
user: login: doonfrs
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-24T16:02:27Z

Review comments:
diff_hunk: @@ -56,12 +56,12 @@ if [ $? -ne 0 ]; then
     exit 1
 fi
 
-echo -e "\n**********Migrating emails**********"
path: migrate.sh
user: login: doonfrs
body: why did you stop the emails migraion?
created_at: 2025-04-24T01:40:51Z
author_association: OWNER
diff_hunk: @@ -56,12 +56,12 @@ if [ $? -ne 0 ]; then
     exit 1
 fi
 
-echo -e "\n**********Migrating emails**********"
path: migrate.sh
user: login: packetdog
body: My apologies- that wasn't intended to be included in the PR. We had no emails on the server, so it was failing. I just omitted it since we didn't need it.
created_at: 2025-04-24T13:50:53Z
author_association: NONE
diff_hunk: @@ -56,12 +56,12 @@ if [ $? -ne 0 ]; then
     exit 1
 fi
 
-echo -e "\n**********Migrating emails**********"
path: migrate.sh
user: login: doonfrs
body: I understand. Could you please update it so I can merge the PR?
created_at: 2025-04-24T16:02:27Z
author_association: OWNER

PR comments:
user: login: packetdog
created_at: 2025-04-20T17:55:09Z
body: Sorry, I found some additional errors. Now we run without any issues. Cheers.
author_association: NONE
user: login: doonfrs
created_at: 2025-04-24T01:47:04Z
body: Thank you for the PR @packetdog 
I have only one comment.

author_association: OWNER