ghcr-manager 0.0.4 → 0.9.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.
Potentially problematic release.
This version of ghcr-manager might be problematic. Click here for more details.
- package/CHANGELOG.md +50 -1
- package/README.md +166 -57
- package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +6 -0
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +113 -0
- package/dist/cleanup-summary/_cleanup-summary.d.ts +40 -0
- package/dist/cleanup-summary/_cleanup-summary.js +40 -0
- package/dist/cleanup-summary/index.d.ts +2 -0
- package/dist/cleanup-summary/index.js +2 -0
- package/dist/cli/_args.d.ts +1 -0
- package/dist/cli/_args.js +3 -0
- package/dist/cli/_cleanup-command.d.ts +1 -0
- package/dist/cli/_cleanup-command.js +63 -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/_logger.d.ts +2 -1
- package/dist/cli/_logger.js +2 -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 +101 -0
- package/dist/cli/_scan-command.js +11 -4
- package/dist/cli/_tag-selector-resolver.d.ts +3 -0
- package/dist/cli/_tag-selector-resolver.js +109 -0
- package/dist/cli/_untag-command.d.ts +1 -0
- package/dist/cli/_untag-command.js +57 -0
- package/dist/cli/index.js +19 -1
- package/dist/config/_service-constants.d.ts +3 -0
- package/dist/config/_service-constants.js +3 -0
- package/dist/{tuning → config}/index.d.ts +3 -0
- package/dist/{tuning → config}/index.js +3 -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 +3 -2
- package/dist/core/index.d.ts +4 -1
- package/dist/core/index.js +2 -1
- package/dist/db/_cleanup-run-writer.d.ts +10 -0
- package/dist/db/_cleanup-run-writer.js +73 -0
- package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
- package/dist/db/_db-merge-cleanup-copy.js +122 -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 +69 -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/_scan-writer.d.ts +3 -1
- package/dist/db/_scan-writer.js +28 -13
- package/dist/db/_snapshot-repository.d.ts +9 -9
- package/dist/db/_snapshot-repository.js +37 -49
- package/dist/db/_sql-placeholders.d.ts +2 -0
- package/dist/db/_sql-placeholders.js +16 -0
- package/dist/db/index.d.ts +5 -0
- package/dist/db/index.js +3 -0
- package/dist/db/planner/_planner-delete-tag-root-targets.d.ts +7 -0
- package/dist/db/planner/_planner-delete-tag-root-targets.js +130 -0
- package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
- package/dist/db/planner/_planner-direct-target-tags.js +47 -0
- package/dist/db/planner/_planner-keep-tagged-root-targets.d.ts +7 -0
- package/dist/db/planner/_planner-keep-tagged-root-targets.js +74 -0
- package/dist/db/planner/_planner-output.d.ts +5 -0
- package/dist/db/planner/_planner-output.js +101 -0
- package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
- package/dist/db/planner/_planner-plan-artifacts.js +211 -0
- package/dist/db/planner/_planner-repository.d.ts +34 -0
- package/dist/db/planner/_planner-repository.js +126 -0
- package/dist/db/planner/_planner-sql.d.ts +12 -0
- package/dist/db/planner/_planner-sql.js +35 -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-tagged-root-targets.d.ts +15 -0
- package/dist/db/planner/_planner-tagged-root-targets.js +19 -0
- package/dist/db/planner/_planner-tagged-targets.d.ts +8 -0
- package/dist/db/planner/_planner-tagged-targets.js +16 -0
- package/dist/db/planner/_planner-types.d.ts +135 -0
- package/dist/db/planner/_planner-types.js +38 -0
- package/dist/db/planner/_planner-untagged-targets.d.ts +9 -0
- package/dist/db/planner/_planner-untagged-targets.js +91 -0
- package/dist/db/planner/index.d.ts +2 -0
- package/dist/db/planner/index.js +1 -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 +47 -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 +51 -0
- package/dist/execute/_types.js +1 -0
- package/dist/execute/_untag-client.d.ts +2 -0
- package/dist/execute/_untag-client.js +71 -0
- package/dist/execute/index.d.ts +5 -0
- package/dist/execute/index.js +3 -0
- package/dist/ingest/github/_manifest-client.d.ts +7 -1
- package/dist/ingest/github/_manifest-client.js +8 -0
- package/dist/ingest/github/_manifest-ingest.js +39 -53
- package/dist/ingest/github/_manifest-kind.d.ts +20 -0
- package/dist/ingest/github/_manifest-kind.js +50 -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 +1 -1
- package/dist/ingest/github/_package-version-page-load.js +8 -5
- package/dist/ingest/github/_packages-client.d.ts +1 -1
- package/dist/ingest/github/_packages-client.js +21 -4
- package/dist/ingest/github/_parallel-paginated-ingest.d.ts +1 -0
- package/dist/ingest/github/_parallel-paginated-ingest.js +2 -2
- package/dist/ingest/github/_shared.d.ts +1 -1
- package/dist/ingest/github/_shared.js +2 -34
- package/dist/ingest/github/index.d.ts +4 -0
- package/dist/ingest/github/index.js +8 -5
- package/package.json +7 -5
- package/resources/sql/schema/001_schema.sql +82 -8
- package/resources/sql/views/001_v_latest_scan_per_package.sql +2 -2
- package/resources/sql/views/003_v_scan_root_manifests.sql +43 -0
- package/resources/sql/views/004_v_digest_derived_tag_relations.sql +51 -0
- package/resources/sql/views/005_v_cleanup_root_closure_members.sql +100 -0
- package/resources/sql/views/006_v_cleanup_blocking_overlaps.sql +42 -0
- package/dist/ingest/github/_paginated-ingest.d.ts +0 -11
- package/dist/ingest/github/_paginated-ingest.js +0 -28
- package/resources/sql/views/003_v_missing_digests_related_manifests.sql +0 -78
- package/resources/sql/views/004_v_manifests_related_manifests.sql +0 -142
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const silentPlannerLogger = {
|
|
2
|
+
trace() { },
|
|
3
|
+
debug() { }
|
|
4
|
+
};
|
|
5
|
+
export function mapPlanRootRow(row) {
|
|
6
|
+
return {
|
|
7
|
+
versionId: row.version_id,
|
|
8
|
+
digest: row.root_digest,
|
|
9
|
+
manifestKind: row.root_manifest_kind ?? undefined,
|
|
10
|
+
reason: row.direct_target_reason,
|
|
11
|
+
selectionMode: row.selection_mode
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function mapPlanTagRows(rows) {
|
|
15
|
+
return rows.map((row) => row.target_tag);
|
|
16
|
+
}
|
|
17
|
+
export function mapClosureManifestRow(row) {
|
|
18
|
+
return {
|
|
19
|
+
sourceVersionId: row.source_version_id,
|
|
20
|
+
sourceDigest: row.source_digest,
|
|
21
|
+
memberVersionId: row.member_version_id,
|
|
22
|
+
memberDigest: row.member_digest,
|
|
23
|
+
memberManifestKind: row.member_manifest_kind ?? undefined,
|
|
24
|
+
hopsFromRoot: row.hops_from_root,
|
|
25
|
+
memberRole: row.member_role
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function mapBlockedRootRow(row) {
|
|
29
|
+
return {
|
|
30
|
+
blockedVersionId: row.blocked_version_id,
|
|
31
|
+
blockedDigest: row.blocked_digest,
|
|
32
|
+
blockingVersionId: row.blocking_version_id,
|
|
33
|
+
blockingDigest: row.blocking_digest,
|
|
34
|
+
overlapDigest: row.overlap_digest,
|
|
35
|
+
overlapManifestKind: row.overlap_manifest_kind ?? undefined,
|
|
36
|
+
reason: row.block_reason
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PlannerSql } from "./_planner-sql.js";
|
|
2
|
+
import { type DeletePlanRoot, type ScanRow } from "./_planner-types.js";
|
|
3
|
+
export declare class PlannerUntaggedTargets {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(sql: PlannerSql);
|
|
6
|
+
getLatestCompletedScan(owner: string, packageName: string): ScanRow;
|
|
7
|
+
listDeleteUntaggedDirectTargetRoots(scanId: number, cutoffTimestamp?: string): DeletePlanRoot[];
|
|
8
|
+
listKeepNUntaggedDirectTargetRoots(scanId: number, keepCount: number, cutoffTimestamp?: string): DeletePlanRoot[];
|
|
9
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mapPlanRootRow } from "./_planner-types.js";
|
|
2
|
+
export class PlannerUntaggedTargets {
|
|
3
|
+
#sql;
|
|
4
|
+
constructor(sql) {
|
|
5
|
+
this.#sql = sql;
|
|
6
|
+
}
|
|
7
|
+
getLatestCompletedScan(owner, packageName) {
|
|
8
|
+
const sql = `
|
|
9
|
+
SELECT scan_id, owner, package_name, scan_completed_at
|
|
10
|
+
FROM v_latest_scan_per_package
|
|
11
|
+
WHERE owner = ?
|
|
12
|
+
AND package_name = ?
|
|
13
|
+
LIMIT 1
|
|
14
|
+
`;
|
|
15
|
+
const row = this.#sql.get(sql, [owner, packageName]);
|
|
16
|
+
if (!row) {
|
|
17
|
+
throw new Error(`database does not contain completed package scan for ${owner}/${packageName}`);
|
|
18
|
+
}
|
|
19
|
+
return row;
|
|
20
|
+
}
|
|
21
|
+
listDeleteUntaggedDirectTargetRoots(scanId, cutoffTimestamp) {
|
|
22
|
+
const cutoffSql = cutoffTimestamp ? "AND created_at < ?" : "";
|
|
23
|
+
const sql = `
|
|
24
|
+
SELECT
|
|
25
|
+
root_version_id AS version_id,
|
|
26
|
+
root_digest,
|
|
27
|
+
root_manifest_kind,
|
|
28
|
+
'delete-untagged' AS direct_target_reason,
|
|
29
|
+
'delete-root' AS selection_mode
|
|
30
|
+
FROM v_scan_root_manifests
|
|
31
|
+
WHERE scan_id = ?
|
|
32
|
+
AND is_tagged = 0
|
|
33
|
+
AND has_ancestor = 0
|
|
34
|
+
${cutoffSql}
|
|
35
|
+
ORDER BY root_digest
|
|
36
|
+
`;
|
|
37
|
+
const rows = this.#sql.all(sql, [
|
|
38
|
+
scanId,
|
|
39
|
+
...(cutoffTimestamp ? [cutoffTimestamp] : [])
|
|
40
|
+
]);
|
|
41
|
+
return rows.map(mapPlanRootRow);
|
|
42
|
+
}
|
|
43
|
+
listKeepNUntaggedDirectTargetRoots(scanId, keepCount, cutoffTimestamp) {
|
|
44
|
+
const cutoffSql = cutoffTimestamp ? "AND pv.created_at < ?" : "";
|
|
45
|
+
const sql = `
|
|
46
|
+
WITH eligible_untagged_roots AS (
|
|
47
|
+
SELECT
|
|
48
|
+
pv.version_id AS version_id,
|
|
49
|
+
m.digest AS root_digest,
|
|
50
|
+
m.manifest_kind AS root_manifest_kind,
|
|
51
|
+
ROW_NUMBER() OVER (
|
|
52
|
+
ORDER BY pv.created_at DESC, pv.version_id DESC, m.digest DESC
|
|
53
|
+
) AS recency_rank
|
|
54
|
+
FROM package_versions pv
|
|
55
|
+
JOIN manifests m
|
|
56
|
+
ON m.scan_id = pv.scan_id
|
|
57
|
+
AND m.version_id = pv.version_id
|
|
58
|
+
WHERE pv.scan_id = ?
|
|
59
|
+
AND NOT EXISTS (
|
|
60
|
+
SELECT 1
|
|
61
|
+
FROM tags t
|
|
62
|
+
WHERE t.scan_id = pv.scan_id
|
|
63
|
+
AND t.version_id = pv.version_id
|
|
64
|
+
)
|
|
65
|
+
AND NOT EXISTS (
|
|
66
|
+
SELECT 1
|
|
67
|
+
FROM manifest_reachability mr
|
|
68
|
+
WHERE mr.scan_id = pv.scan_id
|
|
69
|
+
AND mr.descendant_digest = m.digest
|
|
70
|
+
AND mr.min_distance > 0
|
|
71
|
+
)
|
|
72
|
+
${cutoffSql}
|
|
73
|
+
)
|
|
74
|
+
SELECT
|
|
75
|
+
version_id,
|
|
76
|
+
root_digest,
|
|
77
|
+
root_manifest_kind,
|
|
78
|
+
'keep-n-untagged-overflow' AS direct_target_reason,
|
|
79
|
+
'delete-root' AS selection_mode
|
|
80
|
+
FROM eligible_untagged_roots
|
|
81
|
+
WHERE recency_rank > ?
|
|
82
|
+
ORDER BY root_digest
|
|
83
|
+
`;
|
|
84
|
+
const rows = this.#sql.all(sql, [
|
|
85
|
+
scanId,
|
|
86
|
+
...(cutoffTimestamp ? [cutoffTimestamp] : []),
|
|
87
|
+
keepCount
|
|
88
|
+
]);
|
|
89
|
+
return rows.map(mapPlanRootRow);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PlannerRepository } 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-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-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-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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { deletePackageVersion } from "./_package-version-delete-client.js";
|
|
2
|
+
import { untagRootTags } from "./_untag-client.js";
|
|
3
|
+
export async function executeDeletePlan(plan, options) {
|
|
4
|
+
const deletedPackageVersions = [];
|
|
5
|
+
const untaggedTags = [];
|
|
6
|
+
const directTargetTagSet = new Set(plan.directTargetTags);
|
|
7
|
+
for (const decision of plan.rootDecisions) {
|
|
8
|
+
if (decision.validationStatus !== "untag-only") {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (!options.listRootTags) {
|
|
12
|
+
throw new Error(`execution requires listRootTags support for untag-only root ${decision.digest}`);
|
|
13
|
+
}
|
|
14
|
+
const selectedTags = options
|
|
15
|
+
.listRootTags({
|
|
16
|
+
owner: plan.owner,
|
|
17
|
+
packageName: plan.packageName,
|
|
18
|
+
versionId: decision.versionId,
|
|
19
|
+
digest: decision.digest
|
|
20
|
+
})
|
|
21
|
+
.filter((tag) => directTargetTagSet.has(tag));
|
|
22
|
+
if (selectedTags.length === 0) {
|
|
23
|
+
throw new Error(`no selected tags resolved for untag-only root ${decision.digest}`);
|
|
24
|
+
}
|
|
25
|
+
untaggedTags.push(...(await untagRootTags(plan.owner, plan.packageName, decision.versionId, decision.digest, selectedTags, options)));
|
|
26
|
+
}
|
|
27
|
+
for (const root of plan.fullyDeletableRoots) {
|
|
28
|
+
options.logger.info(`Deleting package version ${root.versionId} for ${plan.owner}/${plan.packageName} (${root.digest})`);
|
|
29
|
+
await deletePackageVersion(plan.owner, plan.packageName, root.versionId, options.token, options.logger, {
|
|
30
|
+
fetchImpl: options.fetchImpl
|
|
31
|
+
});
|
|
32
|
+
deletedPackageVersions.push({
|
|
33
|
+
versionId: root.versionId,
|
|
34
|
+
digest: root.digest
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
owner: plan.owner,
|
|
39
|
+
packageName: plan.packageName,
|
|
40
|
+
scanCompletedAt: plan.scanCompletedAt,
|
|
41
|
+
plannerInputs: plan.plannerInputs,
|
|
42
|
+
deletedPackageVersions,
|
|
43
|
+
untaggedTags,
|
|
44
|
+
blockedRoots: plan.blockedRoots,
|
|
45
|
+
unsupportedUntagRoots: []
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DeleteExecutionLogger, GitHubPackageFetch } from "./_types.js";
|
|
2
|
+
export interface LoadedRegistryManifest {
|
|
3
|
+
digest: string;
|
|
4
|
+
mediaType: string;
|
|
5
|
+
rawJson: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function loadRegistryManifestByDigest(owner: string, packageName: string, digest: string, registryToken: string, logger: DeleteExecutionLogger, runtime?: {
|
|
8
|
+
fetchImpl?: GitHubPackageFetch;
|
|
9
|
+
}): Promise<LoadedRegistryManifest>;
|
|
10
|
+
export declare function putRegistryManifestForTag(owner: string, packageName: string, tag: string, mediaType: string, manifestJson: string, registryToken: string, logger: DeleteExecutionLogger, runtime?: {
|
|
11
|
+
fetchImpl?: GitHubPackageFetch;
|
|
12
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ghcrRegistryBaseUrl } from "../config/index.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { buildHttpErrorMessage, buildTransportErrorMessage, isRetryableStatus, resolveFetch, resolveJsonHeaders, runWithRetry } from "./_http.js";
|
|
4
|
+
const _ACCEPTED_MANIFEST_MEDIA_TYPES = [
|
|
5
|
+
"application/vnd.oci.image.index.v1+json",
|
|
6
|
+
"application/vnd.oci.image.manifest.v1+json",
|
|
7
|
+
"application/vnd.docker.distribution.manifest.list.v2+json",
|
|
8
|
+
"application/vnd.docker.distribution.manifest.v2+json",
|
|
9
|
+
"application/vnd.oci.artifact.manifest.v1+json"
|
|
10
|
+
].join(", ");
|
|
11
|
+
export async function loadRegistryManifestByDigest(owner, packageName, digest, registryToken, logger, runtime) {
|
|
12
|
+
const fetchImpl = resolveFetch(runtime?.fetchImpl);
|
|
13
|
+
const url = new URL(`/v2/${owner}/${packageName}/manifests/${digest}`, ghcrRegistryBaseUrl);
|
|
14
|
+
let response;
|
|
15
|
+
try {
|
|
16
|
+
response = await runWithRetry(`GHCR manifest request for ${digest}`, logger, async () => {
|
|
17
|
+
const manifestResponse = await fetchImpl(url.toString(), {
|
|
18
|
+
headers: {
|
|
19
|
+
Accept: _ACCEPTED_MANIFEST_MEDIA_TYPES,
|
|
20
|
+
Authorization: `Bearer ${registryToken}`,
|
|
21
|
+
"User-Agent": "ghcr-manager"
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
if (!manifestResponse.ok && isRetryableStatus(manifestResponse.status)) {
|
|
25
|
+
throw new Error(await buildHttpErrorMessage(manifestResponse, `GHCR manifest request for ${digest} failed`));
|
|
26
|
+
}
|
|
27
|
+
return manifestResponse;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new Error(buildTransportErrorMessage(error, `GHCR manifest request for ${digest} failed`), {
|
|
32
|
+
cause: error
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(await buildHttpErrorMessage(response, `GHCR manifest request for ${digest} failed`));
|
|
37
|
+
}
|
|
38
|
+
const document = (await response.json());
|
|
39
|
+
const mediaType = document.mediaType ?? resolveJsonHeaders(response);
|
|
40
|
+
if (!mediaType) {
|
|
41
|
+
throw new Error(`manifest response for ${digest} did not include a media type`);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
digest,
|
|
45
|
+
mediaType,
|
|
46
|
+
rawJson: JSON.stringify(document)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function putRegistryManifestForTag(owner, packageName, tag, mediaType, manifestJson, registryToken, logger, runtime) {
|
|
50
|
+
const fetchImpl = resolveFetch(runtime?.fetchImpl);
|
|
51
|
+
const url = new URL(`/v2/${owner}/${packageName}/manifests/${encodeURIComponent(tag)}`, ghcrRegistryBaseUrl);
|
|
52
|
+
let response;
|
|
53
|
+
try {
|
|
54
|
+
response = await runWithRetry(`GHCR manifest put request for tag ${tag}`, logger, async () => {
|
|
55
|
+
const putResponse = await fetchImpl(url.toString(), {
|
|
56
|
+
method: "PUT",
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${registryToken}`,
|
|
59
|
+
"Content-Type": mediaType,
|
|
60
|
+
"User-Agent": "ghcr-manager"
|
|
61
|
+
},
|
|
62
|
+
body: manifestJson
|
|
63
|
+
});
|
|
64
|
+
if (!putResponse.ok && isRetryableStatus(putResponse.status)) {
|
|
65
|
+
throw new Error(await buildHttpErrorMessage(putResponse, `GHCR manifest put request for tag ${tag} failed`));
|
|
66
|
+
}
|
|
67
|
+
return putResponse;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new Error(buildTransportErrorMessage(error, `GHCR manifest put request for tag ${tag} failed`), {
|
|
72
|
+
cause: error
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(await buildHttpErrorMessage(response, `GHCR manifest put request for tag ${tag} failed`));
|
|
77
|
+
}
|
|
78
|
+
return `sha256:${createHash("sha256").update(manifestJson).digest("hex")}`;
|
|
79
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DeleteExecutionLogger, GitHubPackageFetch } from "./_types.js";
|
|
2
|
+
export declare function loadRegistryPushToken(owner: string, packageName: string, token: string, logger: DeleteExecutionLogger, runtime?: {
|
|
3
|
+
fetchImpl?: GitHubPackageFetch;
|
|
4
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ghcrRegistryBaseUrl } from "../config/index.js";
|
|
2
|
+
import { buildHttpErrorMessage, buildTransportErrorMessage, isRetryableStatus, resolveFetch, runWithRetry } from "./_http.js";
|
|
3
|
+
export async function loadRegistryPushToken(owner, packageName, token, logger, runtime) {
|
|
4
|
+
const fetchImpl = resolveFetch(runtime?.fetchImpl);
|
|
5
|
+
const registryUrl = new URL(ghcrRegistryBaseUrl);
|
|
6
|
+
const tokenUrl = new URL("/token", registryUrl);
|
|
7
|
+
tokenUrl.searchParams.set("service", registryUrl.host);
|
|
8
|
+
tokenUrl.searchParams.set("scope", `repository:${owner}/${packageName}:pull,push`);
|
|
9
|
+
let response;
|
|
10
|
+
try {
|
|
11
|
+
response = await runWithRetry("GHCR token request", logger, async () => {
|
|
12
|
+
const tokenResponse = await fetchImpl(tokenUrl.toString(), {
|
|
13
|
+
headers: {
|
|
14
|
+
"User-Agent": "ghcr-manager",
|
|
15
|
+
Authorization: `Basic ${Buffer.from(`${owner}:${token}`).toString("base64")}`
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
if (!tokenResponse.ok && isRetryableStatus(tokenResponse.status)) {
|
|
19
|
+
throw new Error(await buildHttpErrorMessage(tokenResponse, "GHCR token request failed"));
|
|
20
|
+
}
|
|
21
|
+
return tokenResponse;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new Error(buildTransportErrorMessage(error, "GHCR token request failed"), {
|
|
26
|
+
cause: error
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(await buildHttpErrorMessage(response, "GHCR token request failed"));
|
|
31
|
+
}
|
|
32
|
+
const body = (await response.json());
|
|
33
|
+
if (!body.token) {
|
|
34
|
+
throw new Error("GHCR token response did not include a token");
|
|
35
|
+
}
|
|
36
|
+
return body.token;
|
|
37
|
+
}
|