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.

Files changed (137) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/README.md +166 -57
  3. package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +6 -0
  4. package/dist/cleanup-summary/_cleanup-summary-markdown.js +113 -0
  5. package/dist/cleanup-summary/_cleanup-summary.d.ts +40 -0
  6. package/dist/cleanup-summary/_cleanup-summary.js +40 -0
  7. package/dist/cleanup-summary/index.d.ts +2 -0
  8. package/dist/cleanup-summary/index.js +2 -0
  9. package/dist/cli/_args.d.ts +1 -0
  10. package/dist/cli/_args.js +3 -0
  11. package/dist/cli/_cleanup-command.d.ts +1 -0
  12. package/dist/cli/_cleanup-command.js +63 -0
  13. package/dist/cli/_db-merge-command.d.ts +1 -0
  14. package/dist/cli/_db-merge-command.js +41 -0
  15. package/dist/cli/_github-output.d.ts +10 -0
  16. package/dist/cli/_github-output.js +13 -0
  17. package/dist/cli/_logger.d.ts +2 -1
  18. package/dist/cli/_logger.js +2 -0
  19. package/dist/cli/_older-than.d.ts +5 -0
  20. package/dist/cli/_older-than.js +42 -0
  21. package/dist/cli/_planner-options.d.ts +20 -0
  22. package/dist/cli/_planner-options.js +101 -0
  23. package/dist/cli/_scan-command.js +11 -4
  24. package/dist/cli/_tag-selector-resolver.d.ts +3 -0
  25. package/dist/cli/_tag-selector-resolver.js +109 -0
  26. package/dist/cli/_untag-command.d.ts +1 -0
  27. package/dist/cli/_untag-command.js +57 -0
  28. package/dist/cli/index.js +19 -1
  29. package/dist/config/_service-constants.d.ts +3 -0
  30. package/dist/config/_service-constants.js +3 -0
  31. package/dist/{tuning → config}/index.d.ts +3 -0
  32. package/dist/{tuning → config}/index.js +3 -0
  33. package/dist/core/_github-package-owner.d.ts +11 -0
  34. package/dist/core/_github-package-owner.js +45 -0
  35. package/dist/core/_http-error.d.ts +6 -0
  36. package/dist/core/_http-error.js +33 -0
  37. package/dist/core/_types.d.ts +3 -2
  38. package/dist/core/index.d.ts +4 -1
  39. package/dist/core/index.js +2 -1
  40. package/dist/db/_cleanup-run-writer.d.ts +10 -0
  41. package/dist/db/_cleanup-run-writer.js +73 -0
  42. package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
  43. package/dist/db/_db-merge-cleanup-copy.js +122 -0
  44. package/dist/db/_db-merge-history.d.ts +2 -0
  45. package/dist/db/_db-merge-history.js +15 -0
  46. package/dist/db/_db-merge-repository.d.ts +8 -0
  47. package/dist/db/_db-merge-repository.js +95 -0
  48. package/dist/db/_db-merge-scan-copy.d.ts +10 -0
  49. package/dist/db/_db-merge-scan-copy.js +69 -0
  50. package/dist/db/_db-merge-types.d.ts +44 -0
  51. package/dist/db/_db-merge-types.js +1 -0
  52. package/dist/db/_github-actions-run-url.d.ts +1 -0
  53. package/dist/db/_github-actions-run-url.js +9 -0
  54. package/dist/db/_scan-writer.d.ts +3 -1
  55. package/dist/db/_scan-writer.js +28 -13
  56. package/dist/db/_snapshot-repository.d.ts +9 -9
  57. package/dist/db/_snapshot-repository.js +37 -49
  58. package/dist/db/_sql-placeholders.d.ts +2 -0
  59. package/dist/db/_sql-placeholders.js +16 -0
  60. package/dist/db/index.d.ts +5 -0
  61. package/dist/db/index.js +3 -0
  62. package/dist/db/planner/_planner-delete-tag-root-targets.d.ts +7 -0
  63. package/dist/db/planner/_planner-delete-tag-root-targets.js +130 -0
  64. package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
  65. package/dist/db/planner/_planner-direct-target-tags.js +47 -0
  66. package/dist/db/planner/_planner-keep-tagged-root-targets.d.ts +7 -0
  67. package/dist/db/planner/_planner-keep-tagged-root-targets.js +74 -0
  68. package/dist/db/planner/_planner-output.d.ts +5 -0
  69. package/dist/db/planner/_planner-output.js +101 -0
  70. package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
  71. package/dist/db/planner/_planner-plan-artifacts.js +211 -0
  72. package/dist/db/planner/_planner-repository.d.ts +34 -0
  73. package/dist/db/planner/_planner-repository.js +126 -0
  74. package/dist/db/planner/_planner-sql.d.ts +12 -0
  75. package/dist/db/planner/_planner-sql.js +35 -0
  76. package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
  77. package/dist/db/planner/_planner-tag-selectors.js +57 -0
  78. package/dist/db/planner/_planner-tagged-root-targets.d.ts +15 -0
  79. package/dist/db/planner/_planner-tagged-root-targets.js +19 -0
  80. package/dist/db/planner/_planner-tagged-targets.d.ts +8 -0
  81. package/dist/db/planner/_planner-tagged-targets.js +16 -0
  82. package/dist/db/planner/_planner-types.d.ts +135 -0
  83. package/dist/db/planner/_planner-types.js +38 -0
  84. package/dist/db/planner/_planner-untagged-targets.d.ts +9 -0
  85. package/dist/db/planner/_planner-untagged-targets.js +91 -0
  86. package/dist/db/planner/index.d.ts +2 -0
  87. package/dist/db/planner/index.js +1 -0
  88. package/dist/execute/_http.d.ts +7 -0
  89. package/dist/execute/_http.js +48 -0
  90. package/dist/execute/_manifest-detach.d.ts +4 -0
  91. package/dist/execute/_manifest-detach.js +31 -0
  92. package/dist/execute/_package-version-delete-client.d.ts +4 -0
  93. package/dist/execute/_package-version-delete-client.js +34 -0
  94. package/dist/execute/_package-version-page-client.d.ts +14 -0
  95. package/dist/execute/_package-version-page-client.js +64 -0
  96. package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
  97. package/dist/execute/_package-version-tag-source-client.js +65 -0
  98. package/dist/execute/_plan-executor.d.ts +3 -0
  99. package/dist/execute/_plan-executor.js +47 -0
  100. package/dist/execute/_registry-manifest-client.d.ts +12 -0
  101. package/dist/execute/_registry-manifest-client.js +79 -0
  102. package/dist/execute/_registry-token-client.d.ts +4 -0
  103. package/dist/execute/_registry-token-client.js +37 -0
  104. package/dist/execute/_types.d.ts +51 -0
  105. package/dist/execute/_types.js +1 -0
  106. package/dist/execute/_untag-client.d.ts +2 -0
  107. package/dist/execute/_untag-client.js +71 -0
  108. package/dist/execute/index.d.ts +5 -0
  109. package/dist/execute/index.js +3 -0
  110. package/dist/ingest/github/_manifest-client.d.ts +7 -1
  111. package/dist/ingest/github/_manifest-client.js +8 -0
  112. package/dist/ingest/github/_manifest-ingest.js +39 -53
  113. package/dist/ingest/github/_manifest-kind.d.ts +20 -0
  114. package/dist/ingest/github/_manifest-kind.js +50 -0
  115. package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
  116. package/dist/ingest/github/_package-metadata-load.js +45 -0
  117. package/dist/ingest/github/_package-version-page-load.d.ts +1 -1
  118. package/dist/ingest/github/_package-version-page-load.js +8 -5
  119. package/dist/ingest/github/_packages-client.d.ts +1 -1
  120. package/dist/ingest/github/_packages-client.js +21 -4
  121. package/dist/ingest/github/_parallel-paginated-ingest.d.ts +1 -0
  122. package/dist/ingest/github/_parallel-paginated-ingest.js +2 -2
  123. package/dist/ingest/github/_shared.d.ts +1 -1
  124. package/dist/ingest/github/_shared.js +2 -34
  125. package/dist/ingest/github/index.d.ts +4 -0
  126. package/dist/ingest/github/index.js +8 -5
  127. package/package.json +7 -5
  128. package/resources/sql/schema/001_schema.sql +82 -8
  129. package/resources/sql/views/001_v_latest_scan_per_package.sql +2 -2
  130. package/resources/sql/views/003_v_scan_root_manifests.sql +43 -0
  131. package/resources/sql/views/004_v_digest_derived_tag_relations.sql +51 -0
  132. package/resources/sql/views/005_v_cleanup_root_closure_members.sql +100 -0
  133. package/resources/sql/views/006_v_cleanup_blocking_overlaps.sql +42 -0
  134. package/dist/ingest/github/_paginated-ingest.d.ts +0 -11
  135. package/dist/ingest/github/_paginated-ingest.js +0 -28
  136. package/resources/sql/views/003_v_missing_digests_related_manifests.sql +0 -78
  137. 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,2 @@
1
+ export { PlannerRepository } from "./_planner-repository.js";
2
+ export type { DeletePlan, DeletePlanBlockedRoot, DeletePlanClosureManifest, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision } from "./_planner-repository.js";
@@ -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,4 @@
1
+ export declare function buildDetachedManifestClone(rawManifestJson: string, mediaType: string, options: {
2
+ detachedTag: string;
3
+ sourceDigest: string;
4
+ }): string;
@@ -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,3 @@
1
+ import type { DeletePlan } from "../db/index.js";
2
+ import { type DeleteExecutionOptions, type DeleteExecutionSummary } from "./_types.js";
3
+ export declare function executeDeletePlan(plan: DeletePlan, options: DeleteExecutionOptions): Promise<DeleteExecutionSummary>;
@@ -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
+ }