ghcr-cleanup-manager 1.1.0
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 +356 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +5 -0
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +224 -0
- package/dist/cleanup-summary/_cleanup-summary.d.ts +56 -0
- package/dist/cleanup-summary/_cleanup-summary.js +59 -0
- package/dist/cleanup-summary/index.d.ts +2 -0
- package/dist/cleanup-summary/index.js +2 -0
- package/dist/cli/_args.d.ts +7 -0
- package/dist/cli/_args.js +41 -0
- package/dist/cli/_cleanup-command.d.ts +1 -0
- package/dist/cli/_cleanup-command.js +142 -0
- package/dist/cli/_db-merge-command.d.ts +1 -0
- package/dist/cli/_db-merge-command.js +41 -0
- package/dist/cli/_github-output.d.ts +10 -0
- package/dist/cli/_github-output.js +13 -0
- package/dist/cli/_json-output.d.ts +1 -0
- package/dist/cli/_json-output.js +11 -0
- package/dist/cli/_logger.d.ts +10 -0
- package/dist/cli/_logger.js +26 -0
- package/dist/cli/_older-than.d.ts +5 -0
- package/dist/cli/_older-than.js +42 -0
- package/dist/cli/_planner-options.d.ts +20 -0
- package/dist/cli/_planner-options.js +83 -0
- package/dist/cli/_scan-command.d.ts +1 -0
- package/dist/cli/_scan-command.js +39 -0
- package/dist/cli/_tag-selector-resolver.d.ts +3 -0
- package/dist/cli/_tag-selector-resolver.js +132 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +52 -0
- package/dist/config/_service-constants.d.ts +3 -0
- package/dist/config/_service-constants.js +3 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +9 -0
- package/dist/core/_digest-tag.d.ts +2 -0
- package/dist/core/_digest-tag.js +12 -0
- package/dist/core/_github-package-owner.d.ts +11 -0
- package/dist/core/_github-package-owner.js +45 -0
- package/dist/core/_http-error.d.ts +6 -0
- package/dist/core/_http-error.js +33 -0
- package/dist/core/_types.d.ts +54 -0
- package/dist/core/_types.js +8 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +4 -0
- package/dist/db/_cleanup-run-writer.d.ts +10 -0
- package/dist/db/_cleanup-run-writer.js +113 -0
- package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
- package/dist/db/_db-merge-cleanup-copy.js +172 -0
- package/dist/db/_db-merge-history.d.ts +2 -0
- package/dist/db/_db-merge-history.js +15 -0
- package/dist/db/_db-merge-repository.d.ts +8 -0
- package/dist/db/_db-merge-repository.js +95 -0
- package/dist/db/_db-merge-scan-copy.d.ts +10 -0
- package/dist/db/_db-merge-scan-copy.js +70 -0
- package/dist/db/_db-merge-types.d.ts +44 -0
- package/dist/db/_db-merge-types.js +1 -0
- package/dist/db/_github-actions-run-url.d.ts +1 -0
- package/dist/db/_github-actions-run-url.js +9 -0
- package/dist/db/_manifest-reachability.d.ts +2 -0
- package/dist/db/_manifest-reachability.js +158 -0
- package/dist/db/_scan-writer.d.ts +20 -0
- package/dist/db/_scan-writer.js +184 -0
- package/dist/db/_schema.d.ts +2 -0
- package/dist/db/_schema.js +19 -0
- package/dist/db/_snapshot-repository.d.ts +24 -0
- package/dist/db/_snapshot-repository.js +86 -0
- package/dist/db/_sql-placeholders.d.ts +2 -0
- package/dist/db/_sql-placeholders.js +16 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +13 -0
- package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
- package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
- package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +7 -0
- package/dist/db/planner/_planner-direct-target-root-tag-filters.js +41 -0
- package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
- package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +189 -0
- package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
- package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
- package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
- package/dist/db/planner/_planner-direct-target-roots-tagged.js +117 -0
- package/dist/db/planner/_planner-direct-target-roots.d.ts +8 -0
- package/dist/db/planner/_planner-direct-target-roots.js +17 -0
- package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
- package/dist/db/planner/_planner-direct-target-tags.js +54 -0
- package/dist/db/planner/_planner-latest-scan.d.ts +10 -0
- package/dist/db/planner/_planner-latest-scan.js +20 -0
- package/dist/db/planner/_planner-output.d.ts +5 -0
- package/dist/db/planner/_planner-output.js +111 -0
- package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +110 -0
- package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +188 -0
- package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
- package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
- package/dist/db/planner/_planner-plan-artifacts.js +109 -0
- package/dist/db/planner/_planner-repository.d.ts +44 -0
- package/dist/db/planner/_planner-repository.js +105 -0
- package/dist/db/planner/_planner-sql.d.ts +12 -0
- package/dist/db/planner/_planner-sql.js +46 -0
- package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
- package/dist/db/planner/_planner-tag-selectors.js +57 -0
- package/dist/db/planner/_planner-types.d.ts +144 -0
- package/dist/db/planner/_planner-types.js +49 -0
- package/dist/db/planner/index.d.ts +3 -0
- package/dist/db/planner/index.js +2 -0
- package/dist/execute/_http.d.ts +7 -0
- package/dist/execute/_http.js +48 -0
- package/dist/execute/_manifest-detach.d.ts +4 -0
- package/dist/execute/_manifest-detach.js +31 -0
- package/dist/execute/_package-version-delete-client.d.ts +4 -0
- package/dist/execute/_package-version-delete-client.js +34 -0
- package/dist/execute/_package-version-page-client.d.ts +14 -0
- package/dist/execute/_package-version-page-client.js +64 -0
- package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
- package/dist/execute/_package-version-tag-source-client.js +65 -0
- package/dist/execute/_plan-executor.d.ts +3 -0
- package/dist/execute/_plan-executor.js +69 -0
- package/dist/execute/_registry-manifest-client.d.ts +12 -0
- package/dist/execute/_registry-manifest-client.js +79 -0
- package/dist/execute/_registry-token-client.d.ts +4 -0
- package/dist/execute/_registry-token-client.js +37 -0
- package/dist/execute/_types.d.ts +34 -0
- package/dist/execute/_types.js +1 -0
- package/dist/execute/_untag-client.d.ts +2 -0
- package/dist/execute/_untag-client.js +30 -0
- package/dist/execute/index.d.ts +5 -0
- package/dist/execute/index.js +3 -0
- package/dist/ingest/github/_manifest-client.d.ts +14 -0
- package/dist/ingest/github/_manifest-client.js +108 -0
- package/dist/ingest/github/_manifest-ingest.d.ts +3 -0
- package/dist/ingest/github/_manifest-ingest.js +90 -0
- package/dist/ingest/github/_manifest-kind.d.ts +26 -0
- package/dist/ingest/github/_manifest-kind.js +65 -0
- package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
- package/dist/ingest/github/_package-metadata-load.js +45 -0
- package/dist/ingest/github/_package-version-page-load.d.ts +13 -0
- package/dist/ingest/github/_package-version-page-load.js +55 -0
- package/dist/ingest/github/_packages-client.d.ts +10 -0
- package/dist/ingest/github/_packages-client.js +76 -0
- package/dist/ingest/github/_parallel-paginated-ingest.d.ts +12 -0
- package/dist/ingest/github/_parallel-paginated-ingest.js +49 -0
- package/dist/ingest/github/_registry-token-client.d.ts +6 -0
- package/dist/ingest/github/_registry-token-client.js +67 -0
- package/dist/ingest/github/_shared.d.ts +28 -0
- package/dist/ingest/github/_shared.js +70 -0
- package/dist/ingest/github/index.d.ts +11 -0
- package/dist/ingest/github/index.js +29 -0
- package/package.json +78 -0
- package/resources/sql/schema/001_schema.sql +222 -0
- package/resources/sql/views/001_v_latest_scan_per_package.sql +27 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { silentPlannerLogger } from "./_planner-types.js";
|
|
2
|
+
export class PlannerSql {
|
|
3
|
+
#database;
|
|
4
|
+
#logger;
|
|
5
|
+
constructor(database, logger = silentPlannerLogger) {
|
|
6
|
+
this.#database = database;
|
|
7
|
+
this.#logger = logger;
|
|
8
|
+
}
|
|
9
|
+
get database() {
|
|
10
|
+
return this.#database;
|
|
11
|
+
}
|
|
12
|
+
get logger() {
|
|
13
|
+
return this.#logger;
|
|
14
|
+
}
|
|
15
|
+
exec(sql, params = []) {
|
|
16
|
+
this.#traceSql(sql, params);
|
|
17
|
+
const startedAt = Date.now();
|
|
18
|
+
this.#database.prepare(sql).run(...params);
|
|
19
|
+
this.#logger.trace(`SQL exec completed in ${Date.now() - startedAt} ms`);
|
|
20
|
+
}
|
|
21
|
+
get(sql, params) {
|
|
22
|
+
this.#debugSql(sql, params);
|
|
23
|
+
this.#traceSql(sql, params);
|
|
24
|
+
const startedAt = Date.now();
|
|
25
|
+
const row = this.#database.prepare(sql).get(...params);
|
|
26
|
+
this.#logger.debug(`SQL returned ${row === undefined ? "0" : "1"} row(s) in ${Date.now() - startedAt} ms`);
|
|
27
|
+
return row;
|
|
28
|
+
}
|
|
29
|
+
all(sql, params) {
|
|
30
|
+
this.#debugSql(sql, params);
|
|
31
|
+
this.#traceSql(sql, params);
|
|
32
|
+
const startedAt = Date.now();
|
|
33
|
+
const rows = this.#database.prepare(sql).all(...params);
|
|
34
|
+
this.#logger.debug(`SQL returned ${rows.length} row(s) in ${Date.now() - startedAt} ms`);
|
|
35
|
+
return rows;
|
|
36
|
+
}
|
|
37
|
+
traceSql(sql, params) {
|
|
38
|
+
this.#traceSql(sql, params);
|
|
39
|
+
}
|
|
40
|
+
#traceSql(sql, params) {
|
|
41
|
+
this.#logger.trace(`SQL:\n${sql.trim()}\nPARAMS: ${JSON.stringify(params)}`);
|
|
42
|
+
}
|
|
43
|
+
#debugSql(sql, params) {
|
|
44
|
+
this.#logger.debug(`SQL:\n${sql.trim()}\nPARAMS: ${JSON.stringify(params)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
export interface PlannerTagSelectorPredicate {
|
|
3
|
+
sql: string;
|
|
4
|
+
params: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function buildTagSelectorPredicate(database: Database.Database, columnSql: string, selectors: string[], useRegex: boolean): PlannerTagSelectorPredicate;
|
|
7
|
+
export declare function wildcardSelectorToSqlLike(selector: string): string;
|
|
8
|
+
export declare function hasWildcard(selector: string): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function buildTagSelectorPredicate(database, columnSql, selectors, useRegex) {
|
|
2
|
+
if (selectors.length === 0) {
|
|
3
|
+
throw new Error("selectors must not be empty");
|
|
4
|
+
}
|
|
5
|
+
if (useRegex) {
|
|
6
|
+
registerRegexFunction(database);
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
sql: selectors
|
|
10
|
+
.map((selector) => {
|
|
11
|
+
if (useRegex) {
|
|
12
|
+
return `regexp(?, ${columnSql})`;
|
|
13
|
+
}
|
|
14
|
+
return hasWildcard(selector) ? `${columnSql} LIKE ? ESCAPE '\\'` : `${columnSql} = ?`;
|
|
15
|
+
})
|
|
16
|
+
.join(" OR "),
|
|
17
|
+
params: useRegex ? selectors : selectors.map((selector) => wildcardSelectorToSqlLike(selector))
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function wildcardSelectorToSqlLike(selector) {
|
|
21
|
+
if (!hasWildcard(selector)) {
|
|
22
|
+
return selector;
|
|
23
|
+
}
|
|
24
|
+
return selector.replaceAll(/[%_\\*?]/g, (character) => {
|
|
25
|
+
switch (character) {
|
|
26
|
+
case "%":
|
|
27
|
+
case "_":
|
|
28
|
+
case "\\":
|
|
29
|
+
return `\\${character}`;
|
|
30
|
+
case "*":
|
|
31
|
+
return "%";
|
|
32
|
+
case "?":
|
|
33
|
+
return "_";
|
|
34
|
+
default:
|
|
35
|
+
return character;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function hasWildcard(selector) {
|
|
40
|
+
return selector.includes("*") || selector.includes("?");
|
|
41
|
+
}
|
|
42
|
+
function registerRegexFunction(database) {
|
|
43
|
+
const markedDatabase = database;
|
|
44
|
+
if (markedDatabase.__ghcrManagerRegexRegistered) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
markedDatabase.__ghcrManagerRegexCache = new Map();
|
|
48
|
+
database.function("regexp", (pattern, value) => {
|
|
49
|
+
let compiled = markedDatabase.__ghcrManagerRegexCache?.get(pattern);
|
|
50
|
+
if (!compiled) {
|
|
51
|
+
compiled = new RegExp(pattern);
|
|
52
|
+
markedDatabase.__ghcrManagerRegexCache?.set(pattern, compiled);
|
|
53
|
+
}
|
|
54
|
+
return compiled.test(value) ? 1 : 0;
|
|
55
|
+
});
|
|
56
|
+
markedDatabase.__ghcrManagerRegexRegistered = true;
|
|
57
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
interface _PlanRootRow {
|
|
2
|
+
version_id: number;
|
|
3
|
+
root_digest: string;
|
|
4
|
+
root_manifest_kind: string | null;
|
|
5
|
+
direct_target_reason: DeletePlanSelectionReason;
|
|
6
|
+
selection_mode: DeletePlanSelectionMode;
|
|
7
|
+
}
|
|
8
|
+
interface _PlanTagRow {
|
|
9
|
+
target_tag: string;
|
|
10
|
+
}
|
|
11
|
+
interface _ClosureManifestRow {
|
|
12
|
+
source_version_id: number;
|
|
13
|
+
source_digest: string;
|
|
14
|
+
member_version_id: number;
|
|
15
|
+
member_digest: string;
|
|
16
|
+
member_manifest_kind: string | null;
|
|
17
|
+
hops_from_root: number;
|
|
18
|
+
member_role: string;
|
|
19
|
+
}
|
|
20
|
+
interface _BlockedRootRow {
|
|
21
|
+
blocked_version_id: number;
|
|
22
|
+
blocked_digest: string;
|
|
23
|
+
blocking_version_id: number;
|
|
24
|
+
blocking_digest: string;
|
|
25
|
+
overlap_digest: string;
|
|
26
|
+
overlap_manifest_kind: string | null;
|
|
27
|
+
block_reason: DeletePlanBlockReasonCode;
|
|
28
|
+
}
|
|
29
|
+
export interface PlannerLogger {
|
|
30
|
+
trace(message: string): void;
|
|
31
|
+
debug(message: string): void;
|
|
32
|
+
warn?(message: string): void;
|
|
33
|
+
}
|
|
34
|
+
export interface ScanRow {
|
|
35
|
+
scan_id: number;
|
|
36
|
+
owner: string;
|
|
37
|
+
package_name: string;
|
|
38
|
+
scan_completed_at: string;
|
|
39
|
+
}
|
|
40
|
+
export type DeletePlanSelectionMode = "delete-root" | "untag-only";
|
|
41
|
+
export type DeletePlanSelectionReason = "delete-tags-all-tags-selected" | "delete-tags-partial-tag-match" | "delete-untagged" | "keep-n-tagged-overflow" | "keep-n-untagged-overflow";
|
|
42
|
+
export type DeletePlanBlockReasonCode = "overlap-with-retained-root";
|
|
43
|
+
export declare const DeletePlanValidationStatuses: {
|
|
44
|
+
readonly fullyDeletable: "fully-deletable";
|
|
45
|
+
readonly blocked: "blocked";
|
|
46
|
+
readonly untagOnly: "untag-only";
|
|
47
|
+
};
|
|
48
|
+
export type DeletePlanValidationStatus = (typeof DeletePlanValidationStatuses)[keyof typeof DeletePlanValidationStatuses];
|
|
49
|
+
export declare const DeletePlanValidationReasonCodes: {
|
|
50
|
+
readonly untagOnlyPartialTagMatch: "untag-only-partial-tag-match";
|
|
51
|
+
readonly untagOnlyRetainedManifest: "untag-only-retained-manifest";
|
|
52
|
+
readonly fullyDeletableNoRetainedOverlap: "fully-deletable-no-retained-overlap";
|
|
53
|
+
readonly blockedOverlapWithRetainedRoot: "blocked-overlap-with-retained-root";
|
|
54
|
+
};
|
|
55
|
+
export type DeletePlanValidationReasonCode = (typeof DeletePlanValidationReasonCodes)[keyof typeof DeletePlanValidationReasonCodes];
|
|
56
|
+
export interface DeletePlanRoot {
|
|
57
|
+
versionId: number;
|
|
58
|
+
digest: string;
|
|
59
|
+
manifestKind?: ManifestKind;
|
|
60
|
+
reason: DeletePlanSelectionReason;
|
|
61
|
+
selectionMode: DeletePlanSelectionMode;
|
|
62
|
+
}
|
|
63
|
+
export interface DeletePlanClosureManifest {
|
|
64
|
+
sourceVersionId: number;
|
|
65
|
+
sourceDigest: string;
|
|
66
|
+
memberVersionId: number;
|
|
67
|
+
memberDigest: string;
|
|
68
|
+
memberManifestKind?: ManifestKind;
|
|
69
|
+
hopsFromRoot: number;
|
|
70
|
+
memberRole: string;
|
|
71
|
+
}
|
|
72
|
+
export interface DeletePlanBlockedRoot {
|
|
73
|
+
blockedVersionId: number;
|
|
74
|
+
blockedDigest: string;
|
|
75
|
+
blockingVersionId: number;
|
|
76
|
+
blockingDigest: string;
|
|
77
|
+
overlapDigest: string;
|
|
78
|
+
overlapManifestKind?: ManifestKind;
|
|
79
|
+
reason: DeletePlanBlockReasonCode;
|
|
80
|
+
}
|
|
81
|
+
export interface DeletePlanRootDecision {
|
|
82
|
+
versionId: number;
|
|
83
|
+
digest: string;
|
|
84
|
+
manifestKind?: ManifestKind;
|
|
85
|
+
selectionMode: DeletePlanSelectionMode;
|
|
86
|
+
selectionReason: DeletePlanSelectionReason;
|
|
87
|
+
validationStatus: DeletePlanValidationStatus;
|
|
88
|
+
validationReasonCode: DeletePlanValidationReasonCode;
|
|
89
|
+
validationReason: string;
|
|
90
|
+
blockingVersionId?: number;
|
|
91
|
+
blockingDigest?: string;
|
|
92
|
+
overlapDigest?: string;
|
|
93
|
+
overlapManifestKind?: ManifestKind;
|
|
94
|
+
}
|
|
95
|
+
export interface DeletePlanProtectedRoot {
|
|
96
|
+
versionId: number;
|
|
97
|
+
digest: string;
|
|
98
|
+
blocks: Array<{
|
|
99
|
+
blockedVersionId: number;
|
|
100
|
+
blockedDigest: string;
|
|
101
|
+
blockReasonCode: DeletePlanBlockReasonCode;
|
|
102
|
+
overlapDigest: string;
|
|
103
|
+
overlapManifestKind?: ManifestKind;
|
|
104
|
+
}>;
|
|
105
|
+
}
|
|
106
|
+
export interface PlanArtifacts {
|
|
107
|
+
closureManifests: DeletePlanClosureManifest[];
|
|
108
|
+
blockedRoots: DeletePlanBlockedRoot[];
|
|
109
|
+
fullyDeletableRoots: DeletePlanRoot[];
|
|
110
|
+
supportedUntagOnlyRootDigests: Set<string>;
|
|
111
|
+
}
|
|
112
|
+
export interface DeletePlan {
|
|
113
|
+
owner: string;
|
|
114
|
+
packageName: string;
|
|
115
|
+
scanCompletedAt: string;
|
|
116
|
+
plannerInputs: {
|
|
117
|
+
deleteUntagged?: boolean;
|
|
118
|
+
deleteGhostImages?: boolean;
|
|
119
|
+
deletePartialImages?: boolean;
|
|
120
|
+
deleteOrphanedImages?: boolean;
|
|
121
|
+
deleteTags?: string[];
|
|
122
|
+
excludeTags?: string[];
|
|
123
|
+
keepNTagged?: number;
|
|
124
|
+
keepNUntagged?: number;
|
|
125
|
+
useRegex?: boolean;
|
|
126
|
+
olderThan?: string;
|
|
127
|
+
cutoffTimestamp?: string;
|
|
128
|
+
};
|
|
129
|
+
directTargetTags: string[];
|
|
130
|
+
directTargetRoots: DeletePlanRoot[];
|
|
131
|
+
rootDecisions: DeletePlanRootDecision[];
|
|
132
|
+
protectedRoots: DeletePlanProtectedRoot[];
|
|
133
|
+
closureManifests: DeletePlanClosureManifest[];
|
|
134
|
+
blockedRoots: DeletePlanBlockedRoot[];
|
|
135
|
+
fullyDeletableRoots: DeletePlanRoot[];
|
|
136
|
+
collateralTags: string[];
|
|
137
|
+
}
|
|
138
|
+
export declare const silentPlannerLogger: PlannerLogger;
|
|
139
|
+
export declare function mapPlanRootRow(row: _PlanRootRow): DeletePlanRoot;
|
|
140
|
+
export declare function mapPlanTagRows(rows: _PlanTagRow[]): string[];
|
|
141
|
+
export declare function mapClosureManifestRow(row: _ClosureManifestRow): DeletePlanClosureManifest;
|
|
142
|
+
export declare function mapBlockedRootRow(row: _BlockedRootRow): DeletePlanBlockedRoot;
|
|
143
|
+
import type { ManifestKind } from "../../core/index.js";
|
|
144
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const DeletePlanValidationStatuses = {
|
|
2
|
+
fullyDeletable: "fully-deletable",
|
|
3
|
+
blocked: "blocked",
|
|
4
|
+
untagOnly: "untag-only"
|
|
5
|
+
};
|
|
6
|
+
export const DeletePlanValidationReasonCodes = {
|
|
7
|
+
untagOnlyPartialTagMatch: "untag-only-partial-tag-match",
|
|
8
|
+
untagOnlyRetainedManifest: "untag-only-retained-manifest",
|
|
9
|
+
fullyDeletableNoRetainedOverlap: "fully-deletable-no-retained-overlap",
|
|
10
|
+
blockedOverlapWithRetainedRoot: "blocked-overlap-with-retained-root"
|
|
11
|
+
};
|
|
12
|
+
export const silentPlannerLogger = {
|
|
13
|
+
trace() { },
|
|
14
|
+
debug() { }
|
|
15
|
+
};
|
|
16
|
+
export function mapPlanRootRow(row) {
|
|
17
|
+
return {
|
|
18
|
+
versionId: row.version_id,
|
|
19
|
+
digest: row.root_digest,
|
|
20
|
+
manifestKind: (row.root_manifest_kind ?? undefined),
|
|
21
|
+
reason: row.direct_target_reason,
|
|
22
|
+
selectionMode: row.selection_mode
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function mapPlanTagRows(rows) {
|
|
26
|
+
return rows.map((row) => row.target_tag);
|
|
27
|
+
}
|
|
28
|
+
export function mapClosureManifestRow(row) {
|
|
29
|
+
return {
|
|
30
|
+
sourceVersionId: row.source_version_id,
|
|
31
|
+
sourceDigest: row.source_digest,
|
|
32
|
+
memberVersionId: row.member_version_id,
|
|
33
|
+
memberDigest: row.member_digest,
|
|
34
|
+
memberManifestKind: (row.member_manifest_kind ?? undefined),
|
|
35
|
+
hopsFromRoot: row.hops_from_root,
|
|
36
|
+
memberRole: row.member_role
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function mapBlockedRootRow(row) {
|
|
40
|
+
return {
|
|
41
|
+
blockedVersionId: row.blocked_version_id,
|
|
42
|
+
blockedDigest: row.blocked_digest,
|
|
43
|
+
blockingVersionId: row.blocking_version_id,
|
|
44
|
+
blockingDigest: row.blocking_digest,
|
|
45
|
+
overlapDigest: row.overlap_digest,
|
|
46
|
+
overlapManifestKind: (row.overlap_manifest_kind ?? undefined),
|
|
47
|
+
reason: row.block_reason
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { PlannerRepository } from "./_planner-repository.js";
|
|
2
|
+
export { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./_planner-repository.js";
|
|
3
|
+
export type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanBlockedRoot, DeletePlanClosureManifest, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision, DeletePlanSelectionMode, DeletePlanSelectionReason, DeletePlanValidationReasonCode, DeletePlanValidationStatus } from "./_planner-repository.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DeleteExecutionLogger, GitHubPackageFetch, GitHubPackageFetchResponse } from "./_types.js";
|
|
2
|
+
export { buildHttpErrorMessage } from "../core/index.js";
|
|
3
|
+
export declare function runWithRetry<T>(label: string, logger: DeleteExecutionLogger, run: () => Promise<T>): Promise<T>;
|
|
4
|
+
export declare function isRetryableStatus(status: number): boolean;
|
|
5
|
+
export declare function buildTransportErrorMessage(error: unknown, fallback: string): string;
|
|
6
|
+
export declare function resolveFetch(fetchImpl?: GitHubPackageFetch): GitHubPackageFetch;
|
|
7
|
+
export declare function resolveJsonHeaders(response: GitHubPackageFetchResponse): string | undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
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]);
|
|
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
|
+
}
|
|
20
|
+
}
|
|
21
|
+
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(" - ");
|
|
33
|
+
}
|
|
34
|
+
export function resolveFetch(fetchImpl) {
|
|
35
|
+
return fetchImpl ?? fetch;
|
|
36
|
+
}
|
|
37
|
+
export function resolveJsonHeaders(response) {
|
|
38
|
+
return response.headers.get("content-type")?.split(";")[0];
|
|
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
|
+
}
|
|
46
|
+
function sleep(delayMs) {
|
|
47
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const _OCI_MEDIA_TYPES = new Set([
|
|
2
|
+
"application/vnd.oci.artifact.manifest.v1+json",
|
|
3
|
+
"application/vnd.oci.image.index.v1+json",
|
|
4
|
+
"application/vnd.oci.image.manifest.v1+json"
|
|
5
|
+
]);
|
|
6
|
+
const _DETACH_ANNOTATION_KEY = "io.github.ghcr-cleanup-manager.detached-tag";
|
|
7
|
+
export function buildDetachedManifestClone(rawManifestJson, mediaType, options) {
|
|
8
|
+
const parsed = JSON.parse(rawManifestJson);
|
|
9
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10
|
+
throw new Error(`manifest ${options.sourceDigest} is not a JSON object`);
|
|
11
|
+
}
|
|
12
|
+
const clone = structuredClone(parsed);
|
|
13
|
+
if (_OCI_MEDIA_TYPES.has(mediaType)) {
|
|
14
|
+
const annotations = _cloneAnnotations(clone.annotations);
|
|
15
|
+
annotations[_DETACH_ANNOTATION_KEY] = `${options.detachedTag} ${options.sourceDigest}`;
|
|
16
|
+
clone.annotations = annotations;
|
|
17
|
+
}
|
|
18
|
+
return `${JSON.stringify(clone, null, 2)}\n`;
|
|
19
|
+
}
|
|
20
|
+
function _cloneAnnotations(value) {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
const annotations = {};
|
|
25
|
+
for (const [key, annotationValue] of Object.entries(value)) {
|
|
26
|
+
if (typeof annotationValue === "string") {
|
|
27
|
+
annotations[key] = annotationValue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return annotations;
|
|
31
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DeleteExecutionLogger, GitHubPackageFetch } from "./_types.js";
|
|
2
|
+
export declare function deletePackageVersion(owner: string, packageName: string, versionId: number, token: string, logger: DeleteExecutionLogger, runtime?: {
|
|
3
|
+
fetchImpl?: GitHubPackageFetch;
|
|
4
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
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";
|
|
4
|
+
export async function deletePackageVersion(owner, packageName, versionId, token, logger, runtime) {
|
|
5
|
+
const fetchImpl = resolveFetch(runtime?.fetchImpl);
|
|
6
|
+
const ownerURIComponent = await getOwnerURIComponent(fetchImpl, owner, token, logger);
|
|
7
|
+
const url = new URL(`/${ownerURIComponent}/packages/container/${encodeURIComponent(packageName)}/versions/${versionId}`, githubApiBaseUrl).toString();
|
|
8
|
+
let response;
|
|
9
|
+
try {
|
|
10
|
+
response = await runWithRetry(`GitHub package delete request for version ${versionId}`, logger, async () => {
|
|
11
|
+
const deleteResponse = await fetchImpl(url, {
|
|
12
|
+
method: "DELETE",
|
|
13
|
+
headers: {
|
|
14
|
+
Accept: "application/vnd.github+json",
|
|
15
|
+
Authorization: `Bearer ${token}`,
|
|
16
|
+
"User-Agent": "ghcr-cleanup-manager",
|
|
17
|
+
"X-GitHub-Api-Version": githubApiVersion
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
if (!deleteResponse.ok && isRetryableStatus(deleteResponse.status)) {
|
|
21
|
+
throw new Error(await buildHttpErrorMessage(deleteResponse, `GitHub package delete request failed for version ${versionId}`));
|
|
22
|
+
}
|
|
23
|
+
return deleteResponse;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new Error(buildTransportErrorMessage(error, `GitHub package delete request failed for version ${versionId}`), {
|
|
28
|
+
cause: error
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(await buildHttpErrorMessage(response, `GitHub package delete request failed for version ${versionId}`));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DeleteExecutionLogger, GitHubPackageFetch } from "./_types.js";
|
|
2
|
+
export interface GitHubPackageVersionPageItem {
|
|
3
|
+
id: number;
|
|
4
|
+
name?: string;
|
|
5
|
+
metadata?: {
|
|
6
|
+
container?: {
|
|
7
|
+
tags?: string[];
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare function findPackageVersionByDigestAndTag(owner: string, packageName: string, digest: string, tag: string, token: string, logger: DeleteExecutionLogger, runtime?: {
|
|
12
|
+
fetchImpl?: GitHubPackageFetch;
|
|
13
|
+
}): Promise<number>;
|
|
14
|
+
export declare function loadPackageVersionPage(owner: string, packageName: string, page: number, token: string, logger: DeleteExecutionLogger, fetchImpl: GitHubPackageFetch): Promise<GitHubPackageVersionPageItem[]>;
|
|
@@ -0,0 +1,64 @@
|
|
|
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";
|
|
4
|
+
export async function findPackageVersionByDigestAndTag(owner, packageName, digest, tag, token, logger, runtime) {
|
|
5
|
+
const fetchImpl = resolveFetch(runtime?.fetchImpl);
|
|
6
|
+
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
7
|
+
const versionId = await _findPackageVersionByDigestAndTagOnce(owner, packageName, digest, tag, token, logger, fetchImpl);
|
|
8
|
+
if (versionId !== undefined) {
|
|
9
|
+
return versionId;
|
|
10
|
+
}
|
|
11
|
+
if (attempt < 5) {
|
|
12
|
+
logger.warn(`Temporary package version for ${owner}/${packageName}:${tag} (${digest}) not visible yet; retrying lookup ${attempt}/5`);
|
|
13
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`could not find temporary package version for ${owner}/${packageName}:${tag} (${digest})`);
|
|
17
|
+
}
|
|
18
|
+
export async function loadPackageVersionPage(owner, packageName, page, token, logger, fetchImpl) {
|
|
19
|
+
const ownerURIComponent = await getOwnerURIComponent(fetchImpl, owner, token, logger);
|
|
20
|
+
const url = new URL(`/${ownerURIComponent}/packages/container/${encodeURIComponent(packageName)}/versions`, githubApiBaseUrl);
|
|
21
|
+
url.searchParams.set("per_page", "100");
|
|
22
|
+
url.searchParams.set("page", String(page));
|
|
23
|
+
let response;
|
|
24
|
+
try {
|
|
25
|
+
response = await runWithRetry(`GitHub Packages request for page ${page}`, logger, async () => {
|
|
26
|
+
const pageResponse = await fetchImpl(url.toString(), {
|
|
27
|
+
headers: {
|
|
28
|
+
Accept: "application/vnd.github+json",
|
|
29
|
+
Authorization: `Bearer ${token}`,
|
|
30
|
+
"User-Agent": "ghcr-cleanup-manager",
|
|
31
|
+
"X-GitHub-Api-Version": githubApiVersion
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (!pageResponse.ok && isRetryableStatus(pageResponse.status)) {
|
|
35
|
+
throw new Error(await buildHttpErrorMessage(pageResponse, `GitHub Packages request for page ${page} failed`));
|
|
36
|
+
}
|
|
37
|
+
return pageResponse;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error(buildTransportErrorMessage(error, `GitHub Packages request for page ${page} failed`), {
|
|
42
|
+
cause: error
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(await buildHttpErrorMessage(response, `GitHub Packages request for page ${page} failed`));
|
|
47
|
+
}
|
|
48
|
+
return (await response.json());
|
|
49
|
+
}
|
|
50
|
+
async function _findPackageVersionByDigestAndTagOnce(owner, packageName, digest, tag, token, logger, fetchImpl) {
|
|
51
|
+
for (let page = 1;; page += 1) {
|
|
52
|
+
const items = await loadPackageVersionPage(owner, packageName, page, token, logger, fetchImpl);
|
|
53
|
+
if (items.length === 0) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const match = items.find((item) => item.name === digest && item.metadata?.container?.tags?.includes(tag));
|
|
57
|
+
if (match) {
|
|
58
|
+
return match.id;
|
|
59
|
+
}
|
|
60
|
+
if (items.length < 100) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DeleteExecutionLogger, GitHubPackageFetch } from "./_types.js";
|
|
2
|
+
export interface GitHubPackageVersionTagSource {
|
|
3
|
+
tag: string;
|
|
4
|
+
sourceVersionId: number;
|
|
5
|
+
sourceDigest: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function listPresentPackageVersionIds(owner: string, packageName: string, versionIds: number[], token: string, logger: DeleteExecutionLogger, runtime?: {
|
|
8
|
+
fetchImpl?: GitHubPackageFetch;
|
|
9
|
+
}): Promise<number[]>;
|
|
10
|
+
export declare function listPackageVersionTagSources(owner: string, packageName: string, tags: string[], token: string, logger: DeleteExecutionLogger, runtime?: {
|
|
11
|
+
fetchImpl?: GitHubPackageFetch;
|
|
12
|
+
}): Promise<GitHubPackageVersionTagSource[]>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { resolveFetch } from "./_http.js";
|
|
2
|
+
import { loadPackageVersionPage } from "./_package-version-page-client.js";
|
|
3
|
+
export async function listPresentPackageVersionIds(owner, packageName, versionIds, token, logger, runtime) {
|
|
4
|
+
const fetchImpl = resolveFetch(runtime?.fetchImpl);
|
|
5
|
+
const requestedVersionIds = [...new Set(versionIds)];
|
|
6
|
+
if (requestedVersionIds.length === 0) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const requestedVersionIdSet = new Set(requestedVersionIds);
|
|
10
|
+
const matches = new Set();
|
|
11
|
+
for (let page = 1;; page += 1) {
|
|
12
|
+
const items = await loadPackageVersionPage(owner, packageName, page, token, logger, fetchImpl);
|
|
13
|
+
if (items.length === 0) {
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
if (!requestedVersionIdSet.has(item.id)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
matches.add(item.id);
|
|
21
|
+
}
|
|
22
|
+
if (matches.size === requestedVersionIds.length || items.length < 100) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return requestedVersionIds.filter((versionId) => matches.has(versionId));
|
|
27
|
+
}
|
|
28
|
+
export async function listPackageVersionTagSources(owner, packageName, tags, token, logger, runtime) {
|
|
29
|
+
const fetchImpl = resolveFetch(runtime?.fetchImpl);
|
|
30
|
+
const requestedTags = [...new Set(tags)];
|
|
31
|
+
if (requestedTags.length === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const requestedTagSet = new Set(requestedTags);
|
|
35
|
+
const matches = new Map();
|
|
36
|
+
for (let page = 1;; page += 1) {
|
|
37
|
+
const items = await loadPackageVersionPage(owner, packageName, page, token, logger, fetchImpl);
|
|
38
|
+
if (items.length === 0) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
for (const item of items) {
|
|
42
|
+
const itemTags = item.metadata?.container?.tags;
|
|
43
|
+
if (!Array.isArray(itemTags) || typeof item.name !== "string") {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
for (const tag of itemTags) {
|
|
47
|
+
if (!requestedTagSet.has(tag) || matches.has(tag)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
matches.set(tag, {
|
|
51
|
+
tag,
|
|
52
|
+
sourceVersionId: item.id,
|
|
53
|
+
sourceDigest: item.name
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (matches.size === requestedTags.length || items.length < 100) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return requestedTags.flatMap((tag) => {
|
|
62
|
+
const match = matches.get(tag);
|
|
63
|
+
return match ? [match] : [];
|
|
64
|
+
});
|
|
65
|
+
}
|