ghcr-cleanup-manager 1.1.5 → 1.1.6
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 +6 -0
- package/README.md +4 -4
- package/dist/core/_github-package-owner.js +20 -27
- package/dist/core/_github-rest.d.ts +15 -0
- package/dist/core/_github-rest.js +116 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/db/planner/_planner-plan-artifacts.js +9 -1
- package/dist/execute/_http.d.ts +1 -2
- package/dist/execute/_http.js +4 -37
- package/dist/execute/_package-version-delete-client.js +3 -5
- package/dist/execute/_package-version-page-client.js +3 -5
- package/dist/ingest/github/_package-metadata-load.js +5 -20
- package/dist/ingest/github/_package-version-page-load.js +5 -20
- package/dist/ingest/github/_shared.js +2 -35
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ 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.6] - 2026-07-08
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Scan and cleanup now retry GitHub REST rate-limit responses consistently.
|
|
15
|
+
|
|
10
16
|
## [v1.1.5] - 2026-07-02
|
|
11
17
|
|
|
12
18
|
### 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.
|
|
44
|
+
uses: ghcr-manager/ghcr-cleanup-manager@v1.1.6
|
|
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.
|
|
87
|
+
- uses: ghcr-manager/ghcr-cleanup-manager@v1.1.6
|
|
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.
|
|
108
|
+
- uses: ghcr-manager/ghcr-cleanup-manager@v1.1.6
|
|
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.
|
|
130
|
+
- uses: ghcr-manager/ghcr-cleanup-manager@v1.1.6
|
|
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
|
-
|
|
11
|
-
const
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/core/index.js
CHANGED
|
@@ -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, [
|
|
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) {
|
package/dist/execute/_http.d.ts
CHANGED
|
@@ -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;
|
package/dist/execute/_http.js
CHANGED
|
@@ -1,35 +1,11 @@
|
|
|
1
1
|
import { executeRequestRetryCount, executeRequestRetryDelayMs } from "../config/index.js";
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"version": "v1.1.6",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"keywords": [
|