ghcr-cleanup-manager 1.1.5 → 1.1.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [v1.1.7] - 2026-07-23
11
+
12
+ ### Changed
13
+
14
+ - Bump the npm build and development toolchain dependencies.
15
+ - Keep `typescript` pinned to `6.0.3` until `typescript-eslint` supports TypeScript 7.
16
+ - Bump used GitHub actions.
17
+
18
+ ## [v1.1.6] - 2026-07-08
19
+
20
+ ### Fixed
21
+
22
+ - Scan and cleanup now retry GitHub REST rate-limit responses consistently.
23
+
10
24
  ## [v1.1.5] - 2026-07-02
11
25
 
12
26
  ### Fixed
package/README.md CHANGED
@@ -41,7 +41,7 @@ jobs:
41
41
 
42
42
  - name: Preview GHCR cleanup
43
43
  id: ghcr-cleanup-manager
44
- uses: ghcr-manager/ghcr-cleanup-manager@v1.1.5
44
+ uses: ghcr-manager/ghcr-cleanup-manager@v1.1.7
45
45
  with:
46
46
  command: cleanup
47
47
  token: ${{ github.token }}
@@ -84,7 +84,7 @@ The action supports two commands:
84
84
  ### Preview cleanup
85
85
 
86
86
  ```yaml
87
- - uses: ghcr-manager/ghcr-cleanup-manager@v1.1.5
87
+ - uses: ghcr-manager/ghcr-cleanup-manager@v1.1.7
88
88
  with:
89
89
  command: cleanup
90
90
  token: ${{ github.token }}
@@ -105,7 +105,7 @@ The action supports two commands:
105
105
  ### Apply cleanup
106
106
 
107
107
  ```yaml
108
- - uses: ghcr-manager/ghcr-cleanup-manager@v1.1.5
108
+ - uses: ghcr-manager/ghcr-cleanup-manager@v1.1.7
109
109
  with:
110
110
  command: cleanup
111
111
  token: ${{ github.token }}
@@ -127,7 +127,7 @@ off `dry-run`.
127
127
  ### Scan one package
128
128
 
129
129
  ```yaml
130
- - uses: ghcr-manager/ghcr-cleanup-manager@v1.1.5
130
+ - uses: ghcr-manager/ghcr-cleanup-manager@v1.1.7
131
131
  with:
132
132
  command: scan
133
133
  token: ${{ github.token }}
@@ -1,5 +1,6 @@
1
1
  import { githubApiBaseUrl, githubApiVersion, ingestRequestRetryCount, ingestRequestRetryDelayMs } from "../config/index.js";
2
2
  import { buildHttpErrorMessage } from "./_http-error.js";
3
+ import { runGitHubApiWithRetry, throwIfRetryableGitHubApiResponse } from "./_github-rest.js";
3
4
  const _ownerUriComponentByOwner = new Map();
4
5
  export async function getOwnerURIComponent(fetchImpl, owner, token, logger) {
5
6
  const cachedOwnerURIComponent = _ownerUriComponentByOwner.get(owner);
@@ -7,8 +8,8 @@ export async function getOwnerURIComponent(fetchImpl, owner, token, logger) {
7
8
  return cachedOwnerURIComponent;
8
9
  }
9
10
  const url = new URL(`/users/${encodeURIComponent(owner)}`, githubApiBaseUrl).toString();
10
- for (let attempt = 1;; attempt += 1) {
11
- const response = await fetchImpl(url, {
11
+ const response = await runGitHubApiWithRetry("GitHub owner lookup", logger, ingestRequestRetryCount, ingestRequestRetryDelayMs, async () => {
12
+ const ownerResponse = await fetchImpl(url, {
12
13
  headers: {
13
14
  Accept: "application/vnd.github+json",
14
15
  Authorization: `Bearer ${token}`,
@@ -16,30 +17,22 @@ export async function getOwnerURIComponent(fetchImpl, owner, token, logger) {
16
17
  "X-GitHub-Api-Version": githubApiVersion
17
18
  }
18
19
  });
19
- if (response.ok) {
20
- const payload = (await response.json());
21
- if (payload.type === "Organization") {
22
- const ownerURIComponent = `orgs/${encodeURIComponent(owner)}`;
23
- _ownerUriComponentByOwner.set(owner, ownerURIComponent);
24
- return ownerURIComponent;
25
- }
26
- if (payload.type === "User") {
27
- const ownerURIComponent = `users/${encodeURIComponent(owner)}`;
28
- _ownerUriComponentByOwner.set(owner, ownerURIComponent);
29
- return ownerURIComponent;
30
- }
31
- throw new Error(`GitHub owner lookup did not include a supported type`);
32
- }
33
- if (!_isRetryableStatus(response.status) || attempt > ingestRequestRetryCount) {
34
- throw new Error(await buildHttpErrorMessage(response, "GitHub owner lookup failed"));
35
- }
36
- logger.warn(`GitHub owner lookup failed on attempt ${attempt}/${ingestRequestRetryCount + 1}; retrying in ${ingestRequestRetryDelayMs}ms - ${await buildHttpErrorMessage(response, "GitHub owner lookup failed")}`);
37
- await _sleep(ingestRequestRetryDelayMs);
20
+ await throwIfRetryableGitHubApiResponse(ownerResponse, "GitHub owner lookup failed", ingestRequestRetryDelayMs);
21
+ return ownerResponse;
22
+ });
23
+ if (!response.ok) {
24
+ throw new Error(await buildHttpErrorMessage(response, "GitHub owner lookup failed"));
38
25
  }
39
- }
40
- function _isRetryableStatus(status) {
41
- return status === 429 || status === 502 || status === 503 || status === 504;
42
- }
43
- function _sleep(delayMs) {
44
- return new Promise((resolve) => setTimeout(resolve, delayMs));
26
+ const payload = (await response.json());
27
+ if (payload.type === "Organization") {
28
+ const ownerURIComponent = `orgs/${encodeURIComponent(owner)}`;
29
+ _ownerUriComponentByOwner.set(owner, ownerURIComponent);
30
+ return ownerURIComponent;
31
+ }
32
+ if (payload.type === "User") {
33
+ const ownerURIComponent = `users/${encodeURIComponent(owner)}`;
34
+ _ownerUriComponentByOwner.set(owner, ownerURIComponent);
35
+ return ownerURIComponent;
36
+ }
37
+ throw new Error(`GitHub owner lookup did not include a supported type`);
45
38
  }
@@ -0,0 +1,15 @@
1
+ import { type HttpErrorResponse } from "./_http-error.js";
2
+ export interface GitHubApiLogger {
3
+ warn(message: string): void;
4
+ }
5
+ export interface GitHubApiResponse extends HttpErrorResponse {
6
+ ok: boolean;
7
+ }
8
+ export declare class RetryableGitHubApiError extends Error {
9
+ readonly retryAfterMs?: number;
10
+ constructor(message: string, retryAfterMs?: number);
11
+ }
12
+ export declare function runGitHubApiWithRetry<T>(label: string, logger: GitHubApiLogger, retryCount: number, retryDelayMs: number, run: () => Promise<T>): Promise<T>;
13
+ export declare function throwIfRetryableGitHubApiResponse(response: GitHubApiResponse, fallback: string, retryDelayMs: number): Promise<void>;
14
+ export declare function isRetryableGitHubApiStatus(status: number): boolean;
15
+ export declare function buildTransportErrorMessage(error: unknown, fallback: string): string;
@@ -0,0 +1,116 @@
1
+ import { buildHttpErrorMessage } from "./_http-error.js";
2
+ const _RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);
3
+ const _MINUTE_MS = 60_000;
4
+ export class RetryableGitHubApiError extends Error {
5
+ retryAfterMs;
6
+ constructor(message, retryAfterMs) {
7
+ super(message);
8
+ this.name = "RetryableGitHubApiError";
9
+ this.retryAfterMs = retryAfterMs;
10
+ }
11
+ }
12
+ export async function runGitHubApiWithRetry(label, logger, retryCount, retryDelayMs, run) {
13
+ let attempt = 0;
14
+ for (;;) {
15
+ try {
16
+ return await run();
17
+ }
18
+ catch (error) {
19
+ attempt += 1;
20
+ if (attempt > retryCount || !_shouldRetryGitHubApiError(error)) {
21
+ throw error;
22
+ }
23
+ const errorMessage = error instanceof Error ? error.message : String(error);
24
+ const resolvedRetryDelayMs = _resolveRetryDelayMs(error, retryDelayMs);
25
+ logger.warn(`${label} failed on attempt ${attempt}/${retryCount + 1}; retrying in ${resolvedRetryDelayMs}ms - ${errorMessage}`);
26
+ await _sleep(resolvedRetryDelayMs);
27
+ }
28
+ }
29
+ }
30
+ export async function throwIfRetryableGitHubApiResponse(response, fallback, retryDelayMs) {
31
+ if (response.ok) {
32
+ return;
33
+ }
34
+ const errorMessage = await buildHttpErrorMessage(response, fallback);
35
+ if (isRetryableGitHubApiStatus(response.status)) {
36
+ throw new RetryableGitHubApiError(errorMessage);
37
+ }
38
+ const rateLimitRetryAfterMs = _resolveRateLimitRetryAfterMs(response, errorMessage, retryDelayMs);
39
+ if (rateLimitRetryAfterMs !== undefined) {
40
+ throw new RetryableGitHubApiError(errorMessage, rateLimitRetryAfterMs);
41
+ }
42
+ }
43
+ export function isRetryableGitHubApiStatus(status) {
44
+ return _RETRYABLE_STATUS_CODES.has(status);
45
+ }
46
+ export function buildTransportErrorMessage(error, fallback) {
47
+ const details = [fallback];
48
+ details.push(..._collectErrorDetails(error));
49
+ return details.join(" - ");
50
+ }
51
+ function _shouldRetryGitHubApiError(error) {
52
+ return (error instanceof RetryableGitHubApiError ||
53
+ (error instanceof Error && /fetch failed|status 429|status 502|status 503|status 504/.test(error.message)));
54
+ }
55
+ function _resolveRetryDelayMs(error, retryDelayMs) {
56
+ if (error instanceof RetryableGitHubApiError && error.retryAfterMs !== undefined) {
57
+ return error.retryAfterMs;
58
+ }
59
+ return retryDelayMs;
60
+ }
61
+ function _resolveRateLimitRetryAfterMs(response, errorMessage, retryDelayMs) {
62
+ const retryAfterHeader = response.headers.get("retry-after");
63
+ if (retryAfterHeader) {
64
+ const retryAfterSeconds = Number.parseInt(retryAfterHeader, 10);
65
+ if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0) {
66
+ return retryAfterSeconds * 1000;
67
+ }
68
+ }
69
+ const remaining = response.headers.get("x-ratelimit-remaining");
70
+ const reset = response.headers.get("x-ratelimit-reset");
71
+ if (remaining === "0" && reset) {
72
+ const resetEpochSeconds = Number.parseInt(reset, 10);
73
+ if (Number.isFinite(resetEpochSeconds)) {
74
+ return Math.max(resetEpochSeconds * 1000 - Date.now(), retryDelayMs);
75
+ }
76
+ }
77
+ if (/secondary rate limit|API rate limit exceeded/i.test(errorMessage)) {
78
+ return _MINUTE_MS;
79
+ }
80
+ return undefined;
81
+ }
82
+ function _sleep(delayMs) {
83
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
84
+ }
85
+ function _collectErrorDetails(error) {
86
+ if (!(error instanceof Error)) {
87
+ return [String(error)];
88
+ }
89
+ const details = [];
90
+ const seen = new Set();
91
+ let current = error;
92
+ while (current instanceof Error && !seen.has(current)) {
93
+ seen.add(current);
94
+ const message = _formatErrorMessage(current);
95
+ if (message && !details.includes(message)) {
96
+ details.push(message);
97
+ }
98
+ current = current.cause;
99
+ }
100
+ return details.length > 0 ? details : [String(error)];
101
+ }
102
+ function _formatErrorMessage(error) {
103
+ const code = "code" in error && typeof error.code === "string"
104
+ ? error.code
105
+ : undefined;
106
+ if (error.message && code) {
107
+ return `${error.message} (${code})`;
108
+ }
109
+ if (error.message) {
110
+ return error.message;
111
+ }
112
+ if (code) {
113
+ return code;
114
+ }
115
+ return undefined;
116
+ }
@@ -2,5 +2,6 @@ export type { ManifestEdgeKind, ManifestEdgeRecord, ManifestDescriptorRecord, Ma
2
2
  export { ManifestKinds } from "./_types.js";
3
3
  export type { HttpErrorResponse } from "./_http-error.js";
4
4
  export { buildHttpErrorMessage } from "./_http-error.js";
5
+ export { buildTransportErrorMessage, isRetryableGitHubApiStatus, runGitHubApiWithRetry, throwIfRetryableGitHubApiResponse } from "./_github-rest.js";
5
6
  export { getOwnerURIComponent } from "./_github-package-owner.js";
6
7
  export { digestFromDigestTag, isDigestTag } from "./_digest-tag.js";
@@ -1,4 +1,5 @@
1
1
  export { ManifestKinds } from "./_types.js";
2
2
  export { buildHttpErrorMessage } from "./_http-error.js";
3
+ export { buildTransportErrorMessage, isRetryableGitHubApiStatus, runGitHubApiWithRetry, throwIfRetryableGitHubApiResponse } from "./_github-rest.js";
3
4
  export { getOwnerURIComponent } from "./_github-package-owner.js";
4
5
  export { digestFromDigestTag, isDigestTag } from "./_digest-tag.js";
@@ -47,7 +47,15 @@ export class PlannerPlanArtifacts {
47
47
  }
48
48
  #listClosureManifests(scanId) {
49
49
  return this.#sql
50
- .all(_LIST_CLOSURE_MANIFESTS_SQL, [scanId, scanId, scanId, scanId, scanId, scanId, scanId])
50
+ .all(_LIST_CLOSURE_MANIFESTS_SQL, [
51
+ scanId,
52
+ scanId,
53
+ scanId,
54
+ scanId,
55
+ scanId,
56
+ scanId,
57
+ scanId
58
+ ])
51
59
  .map(mapClosureManifestRow);
52
60
  }
53
61
  #listBlockedRoots(scanId) {
@@ -1,7 +1,6 @@
1
1
  import type { DeleteExecutionLogger, GitHubPackageFetch, GitHubPackageFetchResponse } from "./_types.js";
2
- export { buildHttpErrorMessage } from "../core/index.js";
2
+ export { buildHttpErrorMessage, buildTransportErrorMessage } from "../core/index.js";
3
3
  export declare function runWithRetry<T>(label: string, logger: DeleteExecutionLogger, run: () => Promise<T>): Promise<T>;
4
4
  export declare function isRetryableStatus(status: number): boolean;
5
- export declare function buildTransportErrorMessage(error: unknown, fallback: string): string;
6
5
  export declare function resolveFetch(fetchImpl?: GitHubPackageFetch): GitHubPackageFetch;
7
6
  export declare function resolveJsonHeaders(response: GitHubPackageFetchResponse): string | undefined;
@@ -1,35 +1,11 @@
1
1
  import { executeRequestRetryCount, executeRequestRetryDelayMs } from "../config/index.js";
2
- export { buildHttpErrorMessage } from "../core/index.js";
3
- const _RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);
2
+ import { isRetryableGitHubApiStatus, runGitHubApiWithRetry } from "../core/index.js";
3
+ export { buildHttpErrorMessage, buildTransportErrorMessage } from "../core/index.js";
4
4
  export async function runWithRetry(label, logger, run) {
5
- let attempt = 0;
6
- for (;;) {
7
- try {
8
- return await run();
9
- }
10
- catch (error) {
11
- attempt += 1;
12
- if (attempt > executeRequestRetryCount || !_shouldRetryError(error)) {
13
- throw error;
14
- }
15
- const errorMessage = error instanceof Error ? error.message : String(error);
16
- logger.warn(`${label} failed on attempt ${attempt}/${executeRequestRetryCount + 1}; retrying in ${executeRequestRetryDelayMs}ms - ${errorMessage}`);
17
- await sleep(executeRequestRetryDelayMs);
18
- }
19
- }
5
+ return runGitHubApiWithRetry(label, logger, executeRequestRetryCount, executeRequestRetryDelayMs, run);
20
6
  }
21
7
  export function isRetryableStatus(status) {
22
- return _RETRYABLE_STATUS_CODES.has(status);
23
- }
24
- export function buildTransportErrorMessage(error, fallback) {
25
- const details = [fallback];
26
- if (error instanceof Error && error.message) {
27
- details.push(error.message);
28
- }
29
- else {
30
- details.push(String(error));
31
- }
32
- return details.join(" - ");
8
+ return isRetryableGitHubApiStatus(status);
33
9
  }
34
10
  export function resolveFetch(fetchImpl) {
35
11
  return fetchImpl ?? fetch;
@@ -37,12 +13,3 @@ export function resolveFetch(fetchImpl) {
37
13
  export function resolveJsonHeaders(response) {
38
14
  return response.headers.get("content-type")?.split(";")[0];
39
15
  }
40
- function _shouldRetryError(error) {
41
- if (!(error instanceof Error)) {
42
- return false;
43
- }
44
- return /fetch failed|status 429|status 502|status 503|status 504/.test(error.message);
45
- }
46
- function sleep(delayMs) {
47
- return new Promise((resolve) => setTimeout(resolve, delayMs));
48
- }
@@ -1,6 +1,6 @@
1
1
  import { githubApiBaseUrl, githubApiVersion } from "../config/index.js";
2
- import { getOwnerURIComponent } from "../core/index.js";
3
- import { buildHttpErrorMessage, buildTransportErrorMessage, isRetryableStatus, resolveFetch, runWithRetry } from "./_http.js";
2
+ import { buildHttpErrorMessage, buildTransportErrorMessage, getOwnerURIComponent, throwIfRetryableGitHubApiResponse } from "../core/index.js";
3
+ import { resolveFetch, runWithRetry } from "./_http.js";
4
4
  export async function deletePackageVersion(owner, packageName, versionId, token, logger, runtime) {
5
5
  const fetchImpl = resolveFetch(runtime?.fetchImpl);
6
6
  const ownerURIComponent = await getOwnerURIComponent(fetchImpl, owner, token, logger);
@@ -17,9 +17,7 @@ export async function deletePackageVersion(owner, packageName, versionId, token,
17
17
  "X-GitHub-Api-Version": githubApiVersion
18
18
  }
19
19
  });
20
- if (!deleteResponse.ok && isRetryableStatus(deleteResponse.status)) {
21
- throw new Error(await buildHttpErrorMessage(deleteResponse, `GitHub package delete request failed for version ${versionId}`));
22
- }
20
+ await throwIfRetryableGitHubApiResponse(deleteResponse, `GitHub package delete request failed for version ${versionId}`, 1000);
23
21
  return deleteResponse;
24
22
  });
25
23
  }
@@ -1,6 +1,6 @@
1
1
  import { githubApiBaseUrl, githubApiVersion } from "../config/index.js";
2
- import { getOwnerURIComponent } from "../core/index.js";
3
- import { buildHttpErrorMessage, buildTransportErrorMessage, isRetryableStatus, resolveFetch, runWithRetry } from "./_http.js";
2
+ import { buildHttpErrorMessage, buildTransportErrorMessage, getOwnerURIComponent, throwIfRetryableGitHubApiResponse } from "../core/index.js";
3
+ import { resolveFetch, runWithRetry } from "./_http.js";
4
4
  export async function findPackageVersionByDigestAndTag(owner, packageName, digest, tag, token, logger, runtime) {
5
5
  const fetchImpl = resolveFetch(runtime?.fetchImpl);
6
6
  for (let attempt = 1; attempt <= 5; attempt += 1) {
@@ -31,9 +31,7 @@ export async function loadPackageVersionPage(owner, packageName, page, token, lo
31
31
  "X-GitHub-Api-Version": githubApiVersion
32
32
  }
33
33
  });
34
- if (!pageResponse.ok && isRetryableStatus(pageResponse.status)) {
35
- throw new Error(await buildHttpErrorMessage(pageResponse, `GitHub Packages request for page ${page} failed`));
36
- }
34
+ await throwIfRetryableGitHubApiResponse(pageResponse, `GitHub Packages request for page ${page} failed`, 1000);
37
35
  return pageResponse;
38
36
  });
39
37
  }
@@ -1,12 +1,12 @@
1
- import { githubApiBaseUrl, githubApiVersion } from "../../config/index.js";
2
- import { getOwnerURIComponent } from "../../core/index.js";
3
- import { buildFetchTransportErrorMessage, buildHttpErrorMessage, withFetchRetry } from "./_shared.js";
1
+ import { githubApiBaseUrl, githubApiVersion, ingestRequestRetryCount, ingestRequestRetryDelayMs } from "../../config/index.js";
2
+ import { buildHttpErrorMessage, getOwnerURIComponent, runGitHubApiWithRetry, throwIfRetryableGitHubApiResponse } from "../../core/index.js";
3
+ import { buildFetchTransportErrorMessage } from "./_shared.js";
4
4
  export async function loadPackageMetadata(fetchImpl, options) {
5
5
  const ownerURIComponent = await getOwnerURIComponent(fetchImpl, options.owner, options.token, options.logger);
6
6
  const url = new URL(`/${ownerURIComponent}/packages/container/${encodeURIComponent(options.packageName)}`, githubApiBaseUrl).toString();
7
7
  let response;
8
8
  try {
9
- response = await withFetchRetry(async () => {
9
+ response = await runGitHubApiWithRetry("GitHub package metadata request", options.logger, ingestRequestRetryCount, ingestRequestRetryDelayMs, async () => {
10
10
  const packageResponse = await fetchImpl(url, {
11
11
  headers: {
12
12
  Accept: "application/vnd.github+json",
@@ -15,14 +15,8 @@ export async function loadPackageMetadata(fetchImpl, options) {
15
15
  "X-GitHub-Api-Version": githubApiVersion
16
16
  }
17
17
  });
18
- if (!packageResponse.ok && _shouldRetryStatus(packageResponse.status)) {
19
- throw new Error(await buildHttpErrorMessage(packageResponse, "GitHub package metadata request failed"));
20
- }
18
+ await throwIfRetryableGitHubApiResponse(packageResponse, "GitHub package metadata request failed", ingestRequestRetryDelayMs);
21
19
  return packageResponse;
22
- }, {
23
- logger: options.logger,
24
- label: "GitHub package metadata request",
25
- shouldRetry: (error) => _shouldRetryError(error)
26
20
  });
27
21
  }
28
22
  catch (error) {
@@ -34,12 +28,3 @@ export async function loadPackageMetadata(fetchImpl, options) {
34
28
  const payload = (await response.json());
35
29
  return { rawJson: JSON.stringify(payload) };
36
30
  }
37
- function _shouldRetryStatus(status) {
38
- return status === 429 || status === 502 || status === 503 || status === 504;
39
- }
40
- function _shouldRetryError(error) {
41
- if (!(error instanceof Error)) {
42
- return false;
43
- }
44
- return /fetch failed|status 429|status 502|status 503|status 504/.test(error.message);
45
- }
@@ -1,12 +1,12 @@
1
- import { githubApiBaseUrl, githubApiVersion } from "../../config/index.js";
2
- import { getOwnerURIComponent } from "../../core/index.js";
3
- import { buildFetchTransportErrorMessage, buildHttpErrorMessage, withFetchRetry } from "./_shared.js";
1
+ import { githubApiBaseUrl, githubApiVersion, ingestRequestRetryCount, ingestRequestRetryDelayMs } from "../../config/index.js";
2
+ import { buildHttpErrorMessage, getOwnerURIComponent, runGitHubApiWithRetry, throwIfRetryableGitHubApiResponse } from "../../core/index.js";
3
+ import { buildFetchTransportErrorMessage } from "./_shared.js";
4
4
  export async function loadPackageVersionPage(fetchImpl, options, page) {
5
5
  const startTime = Date.now();
6
6
  const url = await buildPackageVersionPageUrl(fetchImpl, options, page);
7
7
  let response;
8
8
  try {
9
- response = await withFetchRetry(async () => {
9
+ response = await runGitHubApiWithRetry(`GitHub Packages request for page ${page}`, options.logger, ingestRequestRetryCount, ingestRequestRetryDelayMs, async () => {
10
10
  const pageResponse = await fetchImpl(url, {
11
11
  headers: {
12
12
  Accept: "application/vnd.github+json",
@@ -15,14 +15,8 @@ export async function loadPackageVersionPage(fetchImpl, options, page) {
15
15
  "X-GitHub-Api-Version": githubApiVersion
16
16
  }
17
17
  });
18
- if (!pageResponse.ok && _shouldRetryStatus(pageResponse.status)) {
19
- throw new Error(await buildHttpErrorMessage(pageResponse, `GitHub Packages request for page ${page} failed`));
20
- }
18
+ await throwIfRetryableGitHubApiResponse(pageResponse, `GitHub Packages request for page ${page} failed`, ingestRequestRetryDelayMs);
21
19
  return pageResponse;
22
- }, {
23
- logger: options.logger,
24
- label: `GitHub Packages request for page ${page}`,
25
- shouldRetry: (error) => _shouldRetryError(error)
26
20
  });
27
21
  }
28
22
  catch (error) {
@@ -44,12 +38,3 @@ async function buildPackageVersionPageUrl(fetchImpl, options, page) {
44
38
  url.searchParams.set("page", String(page));
45
39
  return url.toString();
46
40
  }
47
- function _shouldRetryStatus(status) {
48
- return status === 429 || status === 502 || status === 503 || status === 504;
49
- }
50
- function _shouldRetryError(error) {
51
- if (!(error instanceof Error)) {
52
- return false;
53
- }
54
- return /fetch failed|status 429|status 502|status 503|status 504/.test(error.message);
55
- }
@@ -1,4 +1,5 @@
1
1
  import { ingestRequestRetryCount, ingestRequestRetryDelayMs } from "../../config/index.js";
2
+ import { buildTransportErrorMessage } from "../../core/index.js";
2
3
  export { buildHttpErrorMessage } from "../../core/index.js";
3
4
  export const acceptedManifestMediaTypes = [
4
5
  "application/vnd.oci.image.index.v1+json",
@@ -11,9 +12,7 @@ export async function defaultFetch(input, init) {
11
12
  return fetch(input, init);
12
13
  }
13
14
  export function buildFetchTransportErrorMessage(error, fallback) {
14
- const details = [fallback];
15
- details.push(..._collectErrorDetails(error));
16
- return details.join(" - ");
15
+ return buildTransportErrorMessage(error, fallback);
17
16
  }
18
17
  export async function withFetchRetry(run, options) {
19
18
  let attempt = 0;
@@ -36,35 +35,3 @@ export async function withFetchRetry(run, options) {
36
35
  function _sleep(delayMs) {
37
36
  return new Promise((resolve) => setTimeout(resolve, delayMs));
38
37
  }
39
- function _collectErrorDetails(error) {
40
- if (!(error instanceof Error)) {
41
- return [String(error)];
42
- }
43
- const details = [];
44
- const seen = new Set();
45
- let current = error;
46
- while (current instanceof Error && !seen.has(current)) {
47
- seen.add(current);
48
- const message = _formatErrorMessage(current);
49
- if (message && !details.includes(message)) {
50
- details.push(message);
51
- }
52
- current = current.cause;
53
- }
54
- return details.length > 0 ? details : [String(error)];
55
- }
56
- function _formatErrorMessage(error) {
57
- const code = "code" in error && typeof error.code === "string"
58
- ? error.code
59
- : undefined;
60
- if (error.message && code) {
61
- return `${error.message} (${code})`;
62
- }
63
- if (error.message) {
64
- return error.message;
65
- }
66
- if (code) {
67
- return code;
68
- }
69
- return undefined;
70
- }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "ghcr-cleanup-manager",
3
3
  "description": "GHCR Cleanup Manager: GitHub Action and CLI for safe GitHub Container Registry cleanup and inspection.",
4
4
  "homepage": "https://github.com/ghcr-manager/ghcr-cleanup-manager#readme",
5
- "version": "v1.1.5",
5
+ "version": "v1.1.7",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "keywords": [
@@ -68,7 +68,7 @@
68
68
  "eslint": "^10.2.1",
69
69
  "eslint-plugin-yml": "^3.4.0",
70
70
  "globals": "^17.5.0",
71
- "markdownlint-cli2": "^0.23.0",
71
+ "markdownlint-cli2": "^0.23.1",
72
72
  "prettier": "^3.8.3",
73
73
  "tsx": "^4.20.6",
74
74
  "typescript": "^6.0.3",