shiply-cli 0.27.1 → 0.27.2

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.
Files changed (2) hide show
  1. package/dist/publish.js +12 -3
  2. package/package.json +1 -1
package/dist/publish.js CHANGED
@@ -21,11 +21,17 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21
21
  const backoffMs = (attempt) => Math.min(8000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250);
22
22
  /** fetch() with a per-attempt timeout + retry-with-backoff on transient failures
23
23
  * (network errors, aborts/timeouts, and 5xx gateway statuses). Bodies here are
24
- * strings/Buffers, so re-sending on retry is safe. */
24
+ * strings/Buffers, so re-sending on retry is safe.
25
+ *
26
+ * `alsoRetry`: extra statuses the CALLER knows are safe to retry. The global
27
+ * set excludes 500 to protect non-idempotent creates — but an idempotent
28
+ * presigned PUT (upload) SHOULD retry a 500 (observed: R2 throws transient
29
+ * 500s mid-publish; without this, one blip failed the whole publish). */
25
30
  export async function fetchWithRetry(url, init, opts = {}) {
26
31
  const attempts = opts.attempts ?? 4;
27
32
  const timeoutMs = opts.timeoutMs ?? 60_000;
28
33
  const label = opts.label ?? url;
34
+ const alsoRetry = new Set(opts.alsoRetry ?? []);
29
35
  let lastErr;
30
36
  for (let i = 0; i < attempts; i++) {
31
37
  const ac = new AbortController();
@@ -33,7 +39,7 @@ export async function fetchWithRetry(url, init, opts = {}) {
33
39
  try {
34
40
  const res = await fetch(url, { ...init, signal: ac.signal });
35
41
  clearTimeout(timer);
36
- if (TRANSIENT_STATUS.has(res.status) && i < attempts - 1) {
42
+ if ((TRANSIENT_STATUS.has(res.status) || alsoRetry.has(res.status)) && i < attempts - 1) {
37
43
  process.stderr.write(` ↻ ${label} returned ${res.status} — retrying (${i + 2}/${attempts})…\n`);
38
44
  await sleep(backoffMs(i));
39
45
  continue;
@@ -235,7 +241,10 @@ async function uploadAll(dir, targets) {
235
241
  while (next < targets.length) {
236
242
  const t = targets[next++];
237
243
  const body = await readFile(join(dir, t.path));
238
- const res = await fetchWithRetry(t.url, { method: t.method, headers: t.headers, body }, { label: `upload ${t.path}` });
244
+ const res = await fetchWithRetry(t.url, { method: t.method, headers: t.headers, body },
245
+ // Presigned object PUT = idempotent → a transient 500 (R2 does throw
246
+ // them) must be retried, not fail the whole publish.
247
+ { label: `upload ${t.path}`, alsoRetry: [500] });
239
248
  if (!res.ok) {
240
249
  if (res.status === 403) {
241
250
  throw new Error(`upload failed for ${t.path}: HTTP 403 — presigned URLs likely expired ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shiply-cli",
3
- "version": "0.27.1",
3
+ "version": "0.27.2",
4
4
  "description": "Publish static sites to shiply.now from the command line \u00e2\u20ac\u201d instant web hosting for agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",