@fishawack/lab-env 5.7.0-beta.4 → 5.7.0-beta.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Changelog
2
2
 
3
+ ### 5.7.0-beta.5 (2026-07-03)
4
+
5
+ #### Bug Fixes
6
+
7
+ * allow rekey outside of projects ([94b511b](https://bitbucket.org/fishawackdigital/lab-env/commits/94b511b629fbf80b842a9ab5db9f3671f1f603f0))
8
+ * retry on rate limits and aws errors on key commands ([0244221](https://bitbucket.org/fishawackdigital/lab-env/commits/0244221b8a263256f015380280024c862213b885))
9
+ * safe check for misc bitbucket vars ([cee035c](https://bitbucket.org/fishawackdigital/lab-env/commits/cee035c25f927d1b5f55c00f10fa35f08733e4bf))
10
+
3
11
  ### 5.7.0-beta.4 (2026-07-03)
4
12
 
5
13
  #### Bug Fixes
@@ -3,6 +3,33 @@ const chalk = require("chalk");
3
3
  const { Spinner } = require("./utilities");
4
4
 
5
5
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
6
+ const MAX_RETRIES = 15;
7
+ const INITIAL_DELAY = 2000;
8
+ const MAX_DELAY = 60000;
9
+
10
+ const RETRYABLE_ERRORS = [
11
+ "Throttling",
12
+ "ThrottlingException",
13
+ "TooManyRequestsException",
14
+ "RequestLimitExceeded",
15
+ "ServiceUnavailable",
16
+ "InternalFailure",
17
+ "RequestTimeout",
18
+ "RequestTimeoutException",
19
+ ];
20
+
21
+ function isRetryable(err) {
22
+ if (
23
+ RETRYABLE_ERRORS.includes(err.name) ||
24
+ RETRYABLE_ERRORS.includes(err.Code)
25
+ ) {
26
+ return true;
27
+ }
28
+
29
+ const statusCode = err.$metadata?.httpStatusCode;
30
+
31
+ return statusCode === 429 || statusCode === 503 || statusCode === 500;
32
+ }
6
33
 
7
34
  function formatElapsed(ms) {
8
35
  const secs = Math.floor(ms / 1000);
@@ -78,7 +105,20 @@ module.exports = async function parallelRunner(
78
105
  continue;
79
106
  }
80
107
 
81
- if (state.status === "active") {
108
+ if (state.status === "retrying") {
109
+ const remaining = Math.max(
110
+ 0,
111
+ Math.ceil(
112
+ (state.retryDelay - (Date.now() - state.retryStart)) /
113
+ 1000,
114
+ ),
115
+ );
116
+ lines.push(
117
+ chalk.yellow(
118
+ ` ↻ ${verb}: ${user} - ${state.client} (${state.batchesDone + 1}/${totalBatches}) retry ${state.retryAttempt}/${MAX_RETRIES} ${remaining}s`,
119
+ ),
120
+ );
121
+ } else if (state.status === "active") {
82
122
  const taskElapsed = Date.now() - state.taskStart;
83
123
  let suffix = "";
84
124
  if (taskElapsed > 5000) {
@@ -130,24 +170,58 @@ module.exports = async function parallelRunner(
130
170
  const groupUsers = group.filter((u) => batch.users.includes(u));
131
171
 
132
172
  await Promise.all(
133
- groupUsers.map(async (user) => {
173
+ groupUsers.map(async (user, userIndex) => {
134
174
  const state = groupStates.get(user);
135
175
  state.client = batch.client;
136
- state.status = "active";
137
176
  state.taskStart = Date.now();
138
177
 
139
- try {
140
- const res = await batch.fn(user);
141
- results.get(user).push({ client: batch.client, res });
142
- state.status = "done";
143
- } catch (err) {
144
- state.status = "error";
145
- state.hasError = true;
146
- state.errorMsg = err.message || String(err);
147
- errors.push({ user, client: batch.client, error: err });
148
- } finally {
149
- state.batchesDone++;
178
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
179
+ state.status = "active";
180
+ state.taskStart = Date.now();
181
+
182
+ try {
183
+ const res = await batch.fn(user);
184
+ results
185
+ .get(user)
186
+ .push({ client: batch.client, res });
187
+ state.status = "done";
188
+ state.retryAttempt = 0;
189
+ break;
190
+ } catch (err) {
191
+ if (!isRetryable(err) || attempt === MAX_RETRIES) {
192
+ state.status = "error";
193
+ state.hasError = true;
194
+ state.errorMsg = err.message || String(err);
195
+ errors.push({
196
+ user,
197
+ client: batch.client,
198
+ error: err,
199
+ });
200
+ break;
201
+ }
202
+
203
+ let delay = Math.min(
204
+ INITIAL_DELAY * Math.pow(2, attempt),
205
+ MAX_DELAY,
206
+ );
207
+ // Stagger retries per user to avoid thundering herd
208
+ delay +=
209
+ Math.floor(Math.random() * delay * 0.3) +
210
+ userIndex * 1000;
211
+
212
+ state.status = "retrying";
213
+ state.retryAttempt = attempt + 1;
214
+ state.retryDelay = delay;
215
+ state.retryStart = Date.now();
216
+ throttleCount++;
217
+
218
+ await new Promise((resolve) =>
219
+ setTimeout(resolve, delay),
220
+ );
221
+ }
150
222
  }
223
+
224
+ state.batchesDone++;
151
225
  }),
152
226
  );
153
227
  }
@@ -68,17 +68,17 @@ const bitbucketApi = (module.exports.bitbucketApi =
68
68
  "https://api.bitbucket.org/2.0/repositories");
69
69
 
70
70
  module.exports.apis = {
71
- bbWorkspaceAPI: `${bitbucketApi}/${misc.bitbucket.workspace}`,
71
+ bbWorkspaceAPI: `${bitbucketApi}/${misc?.bitbucket?.workspace}`,
72
72
  };
73
73
 
74
74
  // URLs
75
75
  module.exports.urls = {
76
- bitbucketSSH: `git@bitbucket.org:${misc.bitbucket.workspace}`,
76
+ bitbucketSSH: `git@bitbucket.org:${misc?.bitbucket?.workspace}`,
77
77
  };
78
78
 
79
79
  // Request Headers
80
80
  module.exports.headers = {
81
- bbHeaders: encode(misc.bitbucket.username, misc.bitbucket.token),
81
+ bbHeaders: encode(misc?.bitbucket?.username, misc?.bitbucket?.token),
82
82
  };
83
83
 
84
84
  // Required global node modules
package/globals.js CHANGED
@@ -279,6 +279,7 @@ if (composer && composer.require && composer.require["laravel/framework"]) {
279
279
  args[0] !== "new" &&
280
280
  args[0] !== "key" &&
281
281
  args[0] !== "dekey" &&
282
+ args[0] !== "rekey" &&
282
283
  args[0] !== "lint" &&
283
284
  args[0] !== "workspace" &&
284
285
  args[0] !== "prune"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishawack/lab-env",
3
- "version": "5.7.0-beta.4",
3
+ "version": "5.7.0-beta.5",
4
4
  "description": "Docker manager for FW",
5
5
  "main": "cli.js",
6
6
  "scripts": {