@hexonet/semantic-release-whmcs 5.1.2 → 5.1.3

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.
package/HISTORY.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [5.1.3](https://github.com/centralnicgroup-opensource/rtldev-middleware-semantic-release-whmcs/compare/v5.1.2...v5.1.3) (2025-09-04)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **core:** implement timeout handling for publish operations and improve error handling in compatibility updates ([5fc8b34](https://github.com/centralnicgroup-opensource/rtldev-middleware-semantic-release-whmcs/commit/5fc8b345a8412e0ec0bfa92922fe2939af84224a))
7
+
1
8
  ## [5.1.2](https://github.com/centralnicgroup-opensource/rtldev-middleware-semantic-release-whmcs/compare/v5.1.1...v5.1.2) (2025-09-01)
2
9
 
3
10
 
package/README.md CHANGED
@@ -76,6 +76,7 @@ That said, before you can use this module for publishing new product/module vers
76
76
  | `PUPPETEER_HEADLESS` | **Optional.** Toggle headless mode on/off. by default true. Values: 1,0. |
77
77
  | `GH_TOKEN` | **Optional.** GitHub API authentication token to use for syncing versions. |
78
78
  | `GH_REPO` | **Optional.** GitHub repository name (format: organization/repository) to use for syncing versions. |
79
+ | `useCookieExtension` | **Optional.** Use cookies extension when puppeteer is running to avoid cookie banner disruptions. |
79
80
 
80
81
  ### Options
81
82
 
package/index.js CHANGED
@@ -6,6 +6,7 @@ import githubReleases from "./lib/get-github-releases.js";
6
6
  import marketplaceVersions from "./lib/scrape-marketplace-versions.js";
7
7
 
8
8
  let verified;
9
+ let whmcsPublishResult = null;
9
10
 
10
11
  /**
11
12
  * Called by semantic-release during the verify step
@@ -26,9 +27,30 @@ async function verifyConditions(pluginConfig, context) {
26
27
  */
27
28
  async function publish(pluginConfig, context) {
28
29
  await verifyConditions(pluginConfig, context);
29
- return publishWHMCS(pluginConfig, context);
30
+
31
+ const result = await publishWHMCS(pluginConfig, context);
32
+
33
+ // Store the result globally for success/fail hooks
34
+ whmcsPublishResult = {
35
+ success: !!result,
36
+ version: context.nextRelease?.version,
37
+ result: result,
38
+ };
39
+
40
+ if (result) {
41
+ console.log(`✅ WHMCS: Published version ${context.nextRelease?.version}`);
42
+ } else {
43
+ console.log(`❌ WHMCS: Failed to publish version ${context.nextRelease?.version}`);
44
+ }
45
+
46
+ return result;
30
47
  }
31
48
 
49
+ /**
50
+ * Sync versions from GitHub releases to WHMCS marketplace
51
+ * @param {*} pluginConfig The semantic-release plugin config
52
+ * @param {*} context The context provided by semantic-release
53
+ */
32
54
  async function syncVersions(pluginConfig, context) {
33
55
  await verifyConditions(pluginConfig, context);
34
56
  const releases = await githubReleases(pluginConfig, context);
@@ -43,29 +65,88 @@ async function syncVersions(pluginConfig, context) {
43
65
  releaseDate: release.published_at,
44
66
  };
45
67
  console.log(`Adding missing version ${context.nextRelease.version}`);
46
- await publish(pluginConfig, context);
68
+ const result = await publish(pluginConfig, context);
69
+ console.log(`Published version ${context.nextRelease.version}:`, result);
47
70
  }
48
71
  }
49
72
  }
73
+ return undefined;
50
74
  }
51
75
 
76
+ /**
77
+ * Delete a version from WHMCS marketplace
78
+ * @param {*} pluginConfig The semantic-release plugin config
79
+ * @param {*} context The context provided by semantic-release
80
+ */
52
81
  async function delVersion(pluginConfig, context) {
53
82
  await verifyConditions(pluginConfig, context);
54
- await deleteVersion(pluginConfig, context);
83
+ const result = await deleteVersion(pluginConfig, context);
84
+ return result;
55
85
  }
56
86
 
87
+ /**
88
+ * Update compatibility versions for a product
89
+ * @param {*} pluginConfig The semantic-release plugin config
90
+ * @param {*} context The context provided by semantic-release
91
+ */
57
92
  async function updateCompatibility(pluginConfig, context) {
58
93
  await verifyConditions(pluginConfig, context);
59
- await setCompatibleVersions(pluginConfig, context);
94
+ const result = await setCompatibleVersions(pluginConfig, context);
95
+ return result;
60
96
  }
61
97
 
98
+ // /**
99
+ // * Called by semantic-release when a release succeeds
100
+ // * @param {*} pluginConfig The semantic-release plugin config
101
+ // * @param {*} context The context provided by semantic-release
102
+ // */
103
+ // async function success(pluginConfig, context) {
104
+ // if (whmcsPublishResult) {
105
+ // if (whmcsPublishResult.success) {
106
+ // console.log(`🎉 WHMCS: Successfully published version ${whmcsPublishResult.version} to marketplace`);
107
+ // if (whmcsPublishResult.result?.url) {
108
+ // console.log(`🔗 WHMCS: ${whmcsPublishResult.result.url}`);
109
+ // }
110
+ // } else {
111
+ // console.log(`⚠️ WHMCS: Version ${whmcsPublishResult.version} was NOT published to marketplace (WHMCS publish failed)`);
112
+ // }
113
+ // } else {
114
+ // console.log(`⚠️ WHMCS: No WHMCS publish attempted during this release`);
115
+ // }
116
+ // }
117
+
118
+ /**
119
+ * Called by semantic-release when a release fails
120
+ * @param {*} pluginConfig The semantic-release plugin config
121
+ * @param {*} context The context provided by semantic-release
122
+ */
62
123
  async function fail(pluginConfig, context) {
63
- await verifyConditions(pluginConfig, context);
124
+ if (whmcsPublishResult) {
125
+ if (whmcsPublishResult.success) {
126
+ console.log(`✅ WHMCS: Version ${whmcsPublishResult.version} was successfully published to marketplace`);
127
+ console.log(`ℹ️ WHMCS: Release failed due to other plugins, not WHMCS`);
128
+ } else {
129
+ console.log(`❌ WHMCS: Failed to publish version ${whmcsPublishResult.version} to marketplace`);
130
+ }
131
+ } else {
132
+ console.log(`ℹ️ WHMCS: No WHMCS publish was attempted before failure`);
133
+ }
134
+
135
+ // Also show any WHMCS-specific errors from context
136
+ const { errors } = context;
137
+ const whmcsErrors = errors?.filter(
138
+ (e) => e.message?.toLowerCase().includes("whmcs") || e.message?.toLowerCase().includes("marketplace")
139
+ );
140
+
141
+ if (whmcsErrors?.length > 0) {
142
+ console.log(`🔍 WHMCS: ${whmcsErrors.map((e) => e.message).join(", ")}`);
143
+ }
64
144
  }
65
145
 
66
146
  export default {
67
147
  verifyConditions,
68
148
  publish,
149
+ //success,
69
150
  fail,
70
151
  syncVersions,
71
152
  delVersion,
@@ -88,8 +88,8 @@ export default async (pluginConfig, context) => {
88
88
  }
89
89
 
90
90
  // Click the confirmation button and wait for navigation/alert
91
- await clickAndWaitForResult(page, 'button[type="submit"].btn-styled-red', { navOpts });
92
- await wait(200);
91
+ await clickAndWaitForResult(page, "button.btn-styled-red", { navOpts });
92
+ await wait(300);
93
93
  debug("Checking for alert-success after confirmation...");
94
94
  const result = await waitForSubmitResult(page, { timeout: navOpts.timeout });
95
95
  if (result === "error") {
@@ -103,7 +103,7 @@ export default async (pluginConfig, context) => {
103
103
  } else {
104
104
  // Fallback: check if the version row is gone
105
105
  debug("No success or error alert appeared after delete. Checking if version row is gone...");
106
- await page.waitForSelector("table", { timeout: 5000 });
106
+ await page.waitForSelector("table", { timeout: 1000 });
107
107
  const rowsAfter = await page.$$("tr");
108
108
  for (const row of rowsAfter) {
109
109
  const textContent = await row.evaluate((el) => el.textContent);
package/lib/publish.js CHANGED
@@ -23,6 +23,7 @@ export default async (pluginConfig, context) => {
23
23
  const cleanedNotes = notes.replace(/\[([^[\]]*)\]\([^()]*\)/gm, "$1");
24
24
 
25
25
  let page, püppi, urlbase, productid, gotoOpts, selectorOpts;
26
+ let success = false;
26
27
 
27
28
  debug(`Release Version: ${version}`);
28
29
  debug(`Notes: ${notes}`);
@@ -77,25 +78,42 @@ export default async (pluginConfig, context) => {
77
78
  const result = await waitForSubmitResult(page, { timeout: selectorOpts.timeout || 15000 });
78
79
  if (result === "error") {
79
80
  debug("Publish failed: error alert shown.");
80
- await safeClose(page);
81
- return false;
81
+ success = false;
82
82
  } else if (result === "success") {
83
83
  debug("Publish succeeded.");
84
+ success = true;
84
85
  } else {
85
86
  debug("No success or error alert appeared after submit.");
87
+ success = false;
86
88
  }
87
89
 
88
- await setCompatibleVersions(pluginConfig, context);
90
+ // Call setCompatibleVersions with proper error handling
91
+ if (success) {
92
+ try {
93
+ await setCompatibleVersions(pluginConfig, context);
94
+ debug("Compatible versions set successfully.");
95
+ } catch (compatError) {
96
+ debug("Setting compatible versions failed:", compatError.message);
97
+ // Don't fail the main publish, just log the error
98
+ }
99
+ }
89
100
  } catch (error) {
90
101
  debug("Publishing new product version failed.", error.message);
91
- await safeClose(page);
92
- return false;
102
+ success = false;
103
+ } finally {
104
+ if (page) {
105
+ await safeClose(page);
106
+ }
93
107
  }
94
- debug("Publishing new product version succeeded.");
95
- await safeClose(page);
96
108
 
97
- return {
98
- name: "WHMCS Marketplace Product Version",
99
- url: `${urlbase}/product/${productid}`,
100
- };
109
+ if (success) {
110
+ debug("Publishing new product version succeeded.");
111
+ return {
112
+ name: "WHMCS Marketplace Product Version",
113
+ url: `${urlbase}/product/${productid}`,
114
+ };
115
+ } else {
116
+ debug("Publishing new product version failed.");
117
+ return false;
118
+ }
101
119
  };
@@ -54,7 +54,7 @@ export async function clickAndWaitForResult(page, selector, { navOpts, resultTim
54
54
  let navigated = false;
55
55
  try {
56
56
  await Promise.all([
57
- page.waitForNavigation({ waitUntil: "networkidle0", timeout: navOpts?.timeout || 10000 }),
57
+ page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: navOpts?.timeout || 10000 }),
58
58
  btn.evaluate((el) => el.click()),
59
59
  ]);
60
60
  navigated = true;
package/lib/puppet.js CHANGED
@@ -3,7 +3,7 @@ import resolveConfig from "./resolve-config.js";
3
3
  import debugConfig from "debug";
4
4
  import path from "path";
5
5
  import { fileURLToPath } from "url";
6
- import { waitForNavigationOrSelector, robustType, wait } from "./puppet-utils.js";
6
+ import { waitForNavigationOrSelector, robustType, wait, safeClose } from "./puppet-utils.js";
7
7
  const debug = debugConfig("semantic-release:whmcs");
8
8
 
9
9
  const __filename = fileURLToPath(import.meta.url);
@@ -16,14 +16,14 @@ export default async (context) => {
16
16
  // logger: logger,
17
17
  gotoOpts: {
18
18
  waitUntil: ["load", "domcontentloaded"],
19
- timeout: 20 * 1000,
19
+ timeout: 3 * 1000, // 3 seconds
20
20
  },
21
21
  navOpts: {
22
22
  waitUntil: ["networkidle0"],
23
- timeout: 20 * 1000,
23
+ timeout: 6 * 1000, // 6 seconds
24
24
  },
25
25
  selectorOpts: {
26
- timeout: 20 * 1000,
26
+ timeout: 6 * 1000, // 6 seconds
27
27
  },
28
28
  logger: context.logger,
29
29
  };
@@ -51,7 +51,7 @@ export default async (context) => {
51
51
  }
52
52
 
53
53
  const browser = await puppeteer.launch({
54
- headless: cfg.headless === "1" ? "true" : false,
54
+ headless: cfg.headless === "1" ? true : false,
55
55
  defaultViewport: null, // automatically full-sized
56
56
  args: launchArgs,
57
57
  });
@@ -92,7 +92,7 @@ export default async (context) => {
92
92
  const { redirected, selectorFound, url } = await waitForNavigationOrSelector(page, {
93
93
  urlPart: "/account",
94
94
  selector: ".account-navbar",
95
- timeout: 5000,
95
+ timeout: 15000,
96
96
  });
97
97
  if (!redirected && !selectorFound) {
98
98
  // Check for alert-danger (login error) on the same page
@@ -103,7 +103,7 @@ export default async (context) => {
103
103
  debug("Login failed: no navigation, no selector, and no error alert after submit");
104
104
  }
105
105
  // await page.screenshot({ path: `login-failed-no-redirect-or-selector.png` });
106
- await page.browser().close();
106
+ await safeClose(page);
107
107
  return false;
108
108
  }
109
109
  debug("WHMCS Marketplace login succeeded (redirected: %s, selectorFound: %s)", redirected, selectorFound);
@@ -114,7 +114,7 @@ export default async (context) => {
114
114
  } catch (e) {
115
115
  debug("Screenshot failed", e.message);
116
116
  }
117
- await page.browser().close();
117
+ await safeClose(page);
118
118
  return false;
119
119
  }
120
120
 
@@ -122,7 +122,7 @@ export default async (context) => {
122
122
  let tmp = productid;
123
123
  if (!tmp || !/^[0-9]+$/.test(productid) || !parseInt(productid, 10)) {
124
124
  debug("No or invalid WHMCS Marketplace Product ID provided.");
125
- await page.browser().close();
125
+ await safeClose(page);
126
126
  return false;
127
127
  }
128
128
  tmp = tmp.replace(/(.)/g, "$&\u200E");
@@ -1,11 +1,14 @@
1
- export default ({ env }) => ({
2
- login: env.WHMCSMP_LOGIN || false,
3
- password: env.WHMCSMP_PASSWORD || false,
4
- productid: env.WHMCSMP_PRODUCTID || false,
5
- minversion: env.WHMCSMP_MINVERSION || "7.10",
6
- ghtoken: env.GH_TOKEN || env.GITHUB_TOKEN || false,
7
- ghrepo: env.GH_REPO || env.GITHUB_REPO || false,
8
- headless: env.PUPPETEER_HEADLESS || "1",
9
- debug: (env.DEBUG && /^semantic-release:(\*|whmcs)$/.test(env.DEBUG)) || false,
10
- useCookieExtension: env.USE_COOKIE_EXTENSION || true,
11
- });
1
+ export default (context) => {
2
+ const env = context?.env || process.env;
3
+ return {
4
+ login: env.WHMCSMP_LOGIN || false,
5
+ password: env.WHMCSMP_PASSWORD || false,
6
+ productid: env.WHMCSMP_PRODUCTID || false,
7
+ minversion: env.WHMCSMP_MINVERSION || "7.10",
8
+ ghtoken: env.GH_TOKEN || env.GITHUB_TOKEN || false,
9
+ ghrepo: env.GH_REPO || env.GITHUB_REPO || false,
10
+ headless: env.PUPPETEER_HEADLESS || "1",
11
+ debug: (env.DEBUG && /^semantic-release:(\*|whmcs)$/.test(env.DEBUG)) || false,
12
+ useCookieExtension: env.USE_COOKIE_EXTENSION || true,
13
+ };
14
+ };
@@ -2,6 +2,7 @@ import puppet from "./puppet.js";
2
2
  import debugConfig from "debug";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { robustType, wait, waitForSubmitResult, loginAndNavigate, clickAndWaitForResult } from "./puppet-utils.js";
5
+ import { safeClose } from "./puppet-utils.js";
5
6
  const debug = debugConfig("semantic-release:whmcs");
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
 
@@ -12,10 +13,9 @@ export default async (pluginConfig, context) => {
12
13
  const sep = "+++++++++++++++++++++++++++++++++++++++++++++++++++";
13
14
  const out = `\n${sep}\n${__filename}\n${sep}\n`;
14
15
 
15
- let page, püppi;
16
- const { productid, urlbase, gotoOpts, selectorOpts, minversion } = (context && context.config) || {};
17
-
16
+ let page, püppi, urlbase, productid, gotoOpts, selectorOpts, minversion;
18
17
  debug(out);
18
+ let success = false;
19
19
  try {
20
20
  // Login and navigate to compatibility page
21
21
  ({ page, püppi } = await loginAndNavigate(
@@ -24,7 +24,7 @@ export default async (pluginConfig, context) => {
24
24
  (p) => `${p.config.urlbase}/product/${p.config.productid}/edit#compatibility`,
25
25
  undefined
26
26
  ));
27
- const { urlbase, productid, gotoOpts, selectorOpts, minversion } = püppi.config;
27
+ ({ urlbase, productid, gotoOpts, selectorOpts, minversion } = püppi.config);
28
28
  const url = `${urlbase}/product/${productid}/edit#compatibility`;
29
29
  debug("product page loaded at %s", url);
30
30
 
@@ -69,20 +69,30 @@ export default async (pluginConfig, context) => {
69
69
  const result = await waitForSubmitResult(page, { timeout: gotoOpts.timeout || 10000 });
70
70
  if (result === "error") {
71
71
  debug("Compatibility update failed: error alert shown.");
72
- await page.browser().close();
73
- return false;
72
+ success = false;
74
73
  } else if (result === "success") {
75
74
  debug("Compatibility update succeeded.");
75
+ success = true;
76
76
  } else {
77
77
  debug("No success or error alert appeared after submit.");
78
78
  }
79
79
  } catch (error) {
80
80
  debug("Updating whmcs compatibility list failed.", error.message);
81
- await page.browser().close();
82
- return false;
81
+ success = false;
82
+ } finally {
83
+ if (page) {
84
+ await safeClose(page);
85
+ }
83
86
  }
84
87
 
85
- debug("Updating whmcs compatibility list succeeded.");
86
- await page.browser().close();
87
- return true;
88
+ if (success) {
89
+ debug("Updating whmcs compatibility list succeeded.");
90
+ return {
91
+ name: "WHMCS Marketplace Compatibility Update",
92
+ url: `${urlbase}/product/${productid}/edit#compatibility`,
93
+ };
94
+ } else {
95
+ debug("Updating whmcs compatibility list failed.");
96
+ return false;
97
+ }
88
98
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hexonet/semantic-release-whmcs",
3
3
  "description": "`semantic-release` plugin for auto-publishing on WHMCS marketplace",
4
- "version": "5.1.2",
4
+ "version": "5.1.3",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "publishConfig": {
@@ -40,7 +40,7 @@
40
40
  "nodeArguments": [
41
41
  "--no-warnings"
42
42
  ],
43
- "timeout": "10m"
43
+ "timeout": "5m"
44
44
  },
45
45
  "prettier": {
46
46
  "printWidth": 120,
@@ -77,7 +77,7 @@
77
77
  "ava": "6.4.1",
78
78
  "c8": "^10.0.0",
79
79
  "prettier": "^3.0.0",
80
- "semantic-release": "^24.0.0",
80
+ "semantic-release": "^24.2.7",
81
81
  "semantic-release-teams-notify-plugin": "github:centralnicgroup-opensource/rtldev-middleware-semantic-release-notify-plugin",
82
82
  "stream-buffers": "^3.0.2"
83
83
  },