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.
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,51 @@
1
+ import type { DeletePlan } from "../db/index.js";
2
+ export interface DeletePackageVersionOperation {
3
+ versionId: number;
4
+ digest: string;
5
+ }
6
+ export interface UnsupportedUntagRoot {
7
+ versionId: number;
8
+ digest: string;
9
+ reason: string;
10
+ }
11
+ export interface UntagTagOperation {
12
+ tag: string;
13
+ sourceVersionId: number;
14
+ sourceDigest: string;
15
+ detachedVersionId: number;
16
+ detachedDigest: string;
17
+ }
18
+ export interface DeleteExecutionSummary {
19
+ owner: string;
20
+ packageName: string;
21
+ scanCompletedAt: string;
22
+ plannerInputs: DeletePlan["plannerInputs"];
23
+ deletedPackageVersions: DeletePackageVersionOperation[];
24
+ untaggedTags: UntagTagOperation[];
25
+ blockedRoots: DeletePlan["blockedRoots"];
26
+ unsupportedUntagRoots: UnsupportedUntagRoot[];
27
+ }
28
+ export interface DeleteExecutionOptions {
29
+ token: string;
30
+ logger: DeleteExecutionLogger;
31
+ fetchImpl?: GitHubPackageFetch;
32
+ listRootTags?: (root: {
33
+ owner: string;
34
+ packageName: string;
35
+ versionId: number;
36
+ digest: string;
37
+ }) => string[];
38
+ }
39
+ export interface DeleteExecutionLogger {
40
+ debug(message: string): void;
41
+ info(message: string): void;
42
+ warn(message: string): void;
43
+ error(message: string): void;
44
+ }
45
+ export interface GitHubPackageFetchResponse {
46
+ ok: boolean;
47
+ status: number;
48
+ headers: Headers;
49
+ json(): Promise<unknown>;
50
+ }
51
+ export type GitHubPackageFetch = (input: string, init?: RequestInit) => Promise<GitHubPackageFetchResponse>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { DeleteExecutionOptions, UntagTagOperation } from "./_types.js";
2
+ export declare function untagRootTags(owner: string, packageName: string, sourceVersionId: number, sourceDigest: string, tags: string[], options: DeleteExecutionOptions): Promise<UntagTagOperation[]>;
@@ -0,0 +1,71 @@
1
+ import { buildDetachedManifestClone } from "./_manifest-detach.js";
2
+ import { findPackageVersionByDigestAndTag } from "./_package-version-page-client.js";
3
+ import { listPackageVersionTagSources, listPresentPackageVersionIds } from "./_package-version-tag-source-client.js";
4
+ import { deletePackageVersion } from "./_package-version-delete-client.js";
5
+ import { loadRegistryManifestByDigest, putRegistryManifestForTag } from "./_registry-manifest-client.js";
6
+ import { loadRegistryPushToken } from "./_registry-token-client.js";
7
+ export async function untagRootTags(owner, packageName, sourceVersionId, sourceDigest, tags, options) {
8
+ const registryToken = await loadRegistryPushToken(owner, packageName, options.token, options.logger, {
9
+ fetchImpl: options.fetchImpl
10
+ });
11
+ const sourceManifest = await loadRegistryManifestByDigest(owner, packageName, sourceDigest, registryToken, options.logger, {
12
+ fetchImpl: options.fetchImpl
13
+ });
14
+ const operations = [];
15
+ for (const tag of tags) {
16
+ options.logger.info(`Detaching tag ${owner}/${packageName}:${tag} from ${sourceDigest}`);
17
+ const detachedManifestJson = buildDetachedManifestClone(sourceManifest.rawJson, sourceManifest.mediaType, {
18
+ detachedTag: tag,
19
+ sourceDigest
20
+ });
21
+ const detachedDigest = await putRegistryManifestForTag(owner, packageName, tag, sourceManifest.mediaType, detachedManifestJson, registryToken, options.logger, {
22
+ fetchImpl: options.fetchImpl
23
+ });
24
+ const detachedVersionId = await findPackageVersionByDigestAndTag(owner, packageName, detachedDigest, tag, options.token, options.logger, {
25
+ fetchImpl: options.fetchImpl
26
+ });
27
+ await deletePackageVersion(owner, packageName, detachedVersionId, options.token, options.logger, {
28
+ fetchImpl: options.fetchImpl
29
+ });
30
+ await _assertTagRemoved(owner, packageName, tag, options);
31
+ await _assertVersionRemoved(owner, packageName, detachedVersionId, options);
32
+ operations.push({
33
+ tag,
34
+ sourceVersionId,
35
+ sourceDigest,
36
+ detachedVersionId,
37
+ detachedDigest
38
+ });
39
+ }
40
+ return operations;
41
+ }
42
+ async function _assertTagRemoved(owner, packageName, tag, options) {
43
+ for (let attempt = 1; attempt <= 5; attempt += 1) {
44
+ const remaining = await listPackageVersionTagSources(owner, packageName, [tag], options.token, options.logger, {
45
+ fetchImpl: options.fetchImpl
46
+ });
47
+ if (remaining.length === 0) {
48
+ return;
49
+ }
50
+ if (attempt < 5) {
51
+ options.logger.warn(`Tag ${owner}/${packageName}:${tag} is still visible after untag; retrying check ${attempt}/5`);
52
+ await new Promise((resolve) => setTimeout(resolve, 1000));
53
+ }
54
+ }
55
+ throw new Error(`tag ${owner}/${packageName}:${tag} is still visible after untag`);
56
+ }
57
+ async function _assertVersionRemoved(owner, packageName, versionId, options) {
58
+ for (let attempt = 1; attempt <= 5; attempt += 1) {
59
+ const presentVersionIds = await listPresentPackageVersionIds(owner, packageName, [versionId], options.token, options.logger, {
60
+ fetchImpl: options.fetchImpl
61
+ });
62
+ if (presentVersionIds.length === 0) {
63
+ return;
64
+ }
65
+ if (attempt < 5) {
66
+ options.logger.warn(`Temporary package version ${owner}/${packageName}#${versionId} is still visible after untag; retrying check ${attempt}/5`);
67
+ await new Promise((resolve) => setTimeout(resolve, 1000));
68
+ }
69
+ }
70
+ throw new Error(`temporary package version ${owner}/${packageName}#${versionId} is still visible after untag`);
71
+ }
@@ -0,0 +1,5 @@
1
+ export { executeDeletePlan } from "./_plan-executor.js";
2
+ export { listPackageVersionTagSources, listPresentPackageVersionIds } from "./_package-version-tag-source-client.js";
3
+ export { untagRootTags } from "./_untag-client.js";
4
+ export type { GitHubPackageVersionTagSource } from "./_package-version-tag-source-client.js";
5
+ export type { DeleteExecutionSummary, UntagTagOperation } from "./_types.js";
@@ -0,0 +1,3 @@
1
+ export { executeDeletePlan } from "./_plan-executor.js";
2
+ export { listPackageVersionTagSources, listPresentPackageVersionIds } from "./_package-version-tag-source-client.js";
3
+ export { untagRootTags } from "./_untag-client.js";
@@ -1,8 +1,14 @@
1
1
  import type { ManifestDescriptorRecord, ManifestEdgeRecord, ManifestRecord } from "../../core/index.js";
2
2
  import { type FetchLike, type GitHubScanOptions } from "./_shared.js";
3
+ type _LoadedManifestRecord = Omit<ManifestRecord, "versionId">;
3
4
  export declare function loadManifestGraph(fetchImpl: FetchLike, registryBaseUrl: string, digest: string, registryToken: string, options: GitHubScanOptions): Promise<{
4
- record: ManifestRecord;
5
+ record: _LoadedManifestRecord;
5
6
  descriptorRecords: ManifestDescriptorRecord[];
6
7
  edgeRecords: ManifestEdgeRecord[];
7
8
  rawJson: string;
8
9
  }>;
10
+ export declare function buildManifestRelations(digest: string, rawJson: string): {
11
+ descriptorRecords: ManifestDescriptorRecord[];
12
+ edgeRecords: ManifestEdgeRecord[];
13
+ };
14
+ export {};
@@ -1,3 +1,4 @@
1
+ import { classifyManifestKind } from "./_manifest-kind.js";
1
2
  import { acceptedManifestMediaTypes, buildFetchTransportErrorMessage, buildHttpErrorMessage, withFetchRetry } from "./_shared.js";
2
3
  export async function loadManifestGraph(fetchImpl, registryBaseUrl, digest, registryToken, options) {
3
4
  const startTime = Date.now();
@@ -42,12 +43,19 @@ export async function loadManifestGraph(fetchImpl, registryBaseUrl, digest, regi
42
43
  rawJson,
43
44
  record: {
44
45
  digest,
46
+ manifestKind: classifyManifestKind(document),
45
47
  mediaType,
46
48
  artifactType: document.artifactType,
47
49
  configMediaType: document.config?.mediaType,
48
50
  subjectDigest: document.subject?.digest,
49
51
  annotations: document.annotations
50
52
  },
53
+ ...buildManifestRelations(digest, rawJson)
54
+ };
55
+ }
56
+ export function buildManifestRelations(digest, rawJson) {
57
+ const document = JSON.parse(rawJson);
58
+ return {
51
59
  descriptorRecords: _buildDescriptorRecords(digest, document),
52
60
  edgeRecords: _buildEdges(digest, document)
53
61
  };
@@ -1,53 +1,37 @@
1
- import { manifestFetchConcurrency, manifestIngestProgressStepRatio } from "../../tuning/index.js";
2
- import { loadManifestGraph } from "./_manifest-client.js";
1
+ import { manifestFetchConcurrency, manifestIngestProgressStepRatio } from "../../config/index.js";
2
+ import { buildManifestRelations, loadManifestGraph } from "./_manifest-client.js";
3
3
  import { loadRegistryPullToken } from "./_registry-token-client.js";
4
4
  export async function ingestManifests(fetchImpl, registryBaseUrl, options, writer, repository, scanId) {
5
- const pendingDigests = repository.listPackageVersionDigests(scanId);
6
- const initialDigestCount = pendingDigests.length;
7
- const progressStep = Math.max(1, Math.ceil(initialDigestCount * manifestIngestProgressStepRatio));
8
- const queuedDigests = new Set(pendingDigests);
9
- const fetchedDigests = new Set();
10
- const persistedDigests = new Set();
5
+ const manifests = repository.listPackageVersionManifestRefs(scanId);
6
+ const totalDigestCount = manifests.length;
7
+ const progressStep = Math.max(1, Math.ceil(totalDigestCount * manifestIngestProgressStepRatio));
11
8
  const registryPullTokenState = {};
12
- options.logger.info(`Fetching manifests for ${pendingDigests.length} package versions`);
9
+ options.logger.info(`Fetching manifests for ${totalDigestCount} package versions`);
13
10
  let completed = 0;
14
- const edgeRecords = [];
11
+ let nextManifestIndex = 0;
15
12
  const activeLoads = new Set();
16
- while (pendingDigests.length > 0 || activeLoads.size > 0) {
17
- while (pendingDigests.length > 0 && activeLoads.size < manifestFetchConcurrency) {
18
- const digest = pendingDigests.shift();
19
- if (!digest || fetchedDigests.has(digest)) {
20
- continue;
21
- }
22
- const load = _loadQueuedManifest(digest, fetchImpl, registryBaseUrl, options, writer, pendingDigests, queuedDigests, fetchedDigests, persistedDigests, edgeRecords, completed, async () => (await _getRegistryPullToken(fetchImpl, registryBaseUrl, options, registryPullTokenState)).token, () => {
13
+ while (nextManifestIndex < manifests.length || activeLoads.size > 0) {
14
+ while (nextManifestIndex < manifests.length && activeLoads.size < manifestFetchConcurrency) {
15
+ const load = _fetchManifest(manifests[nextManifestIndex], fetchImpl, registryBaseUrl, options, writer, completed, async () => (await _getRegistryPullToken(fetchImpl, registryBaseUrl, options, registryPullTokenState)).token, () => {
23
16
  completed += 1;
24
- if (completed % progressStep === 0 || pendingDigests.length === 0) {
25
- options.logger.info(`Fetched manifests ${completed}/${queuedDigests.size}`);
17
+ if (completed % progressStep === 0 || completed === totalDigestCount) {
18
+ options.logger.info(`Fetched manifests ${completed}/${totalDigestCount}`);
26
19
  }
27
20
  }).finally(() => {
28
21
  activeLoads.delete(load);
29
22
  });
23
+ nextManifestIndex += 1;
30
24
  activeLoads.add(load);
31
25
  }
32
26
  if (activeLoads.size > 0) {
33
27
  await Promise.race(activeLoads);
34
28
  }
35
29
  }
36
- options.logger.info(`Starting manifest graph processing for ${edgeRecords.length} edges`);
37
- let persistedEdgeCount = 0;
38
- for (const edge of edgeRecords) {
39
- if (!persistedDigests.has(edge.parentDigest) || !persistedDigests.has(edge.childDigest)) {
40
- continue;
41
- }
42
- writer.insertManifestEdge(edge);
43
- persistedEdgeCount += 1;
44
- }
45
- options.logger.info(`Inserted ${persistedEdgeCount} manifest edges; rebuilding reachability`);
46
- writer.rebuildManifestReachability();
47
- options.logger.info("Completed manifest graph processing");
30
+ _processManifestPayloads(options, writer, repository, scanId);
48
31
  }
49
- async function _loadQueuedManifest(digest, fetchImpl, registryBaseUrl, options, writer, pendingDigests, queuedDigests, fetchedDigests, persistedDigests, edgeRecords, completed, getRegistryToken, onComplete) {
50
- options.logger.debug(`Fetching manifest ${completed + 1}/${queuedDigests.size}: ${digest}`);
32
+ async function _fetchManifest(manifestRef, fetchImpl, registryBaseUrl, options, writer, completed, getRegistryToken, onComplete) {
33
+ const { versionId, digest } = manifestRef;
34
+ options.logger.debug(`Fetching manifest ${completed + 1}: ${digest}`);
51
35
  let manifest;
52
36
  try {
53
37
  manifest = await loadManifestGraph(fetchImpl, registryBaseUrl, digest, await getRegistryToken(), options);
@@ -55,27 +39,36 @@ async function _loadQueuedManifest(digest, fetchImpl, registryBaseUrl, options,
55
39
  catch (error) {
56
40
  if (_isMissingManifestError(error)) {
57
41
  options.logger.warn(`Skipping missing GHCR manifest ${digest}`);
58
- fetchedDigests.add(digest);
59
42
  onComplete();
60
43
  return;
61
44
  }
62
45
  throw error;
63
46
  }
64
- writer.insertManifest(manifest.record);
65
- persistedDigests.add(manifest.record.digest);
47
+ writer.insertManifest({ versionId, ...manifest.record });
66
48
  writer.insertManifestPayload(manifest.record.digest, manifest.rawJson);
67
- for (const descriptor of manifest.descriptorRecords) {
68
- writer.insertManifestDescriptor(descriptor);
69
- _enqueueDigest(descriptor.childDigest, pendingDigests, queuedDigests, fetchedDigests);
70
- }
71
- edgeRecords.push(...manifest.edgeRecords);
72
- for (const edge of manifest.edgeRecords) {
73
- _enqueueDigest(edge.parentDigest, pendingDigests, queuedDigests, fetchedDigests);
74
- _enqueueDigest(edge.childDigest, pendingDigests, queuedDigests, fetchedDigests);
75
- }
76
- fetchedDigests.add(digest);
77
49
  onComplete();
78
50
  }
51
+ function _processManifestPayloads(options, writer, repository, scanId) {
52
+ const digests = new Set(repository.listManifestDigests(scanId));
53
+ const payloads = repository.listManifestPayloads(scanId);
54
+ options.logger.info(`Starting manifest graph processing for ${payloads.length} manifest payloads`);
55
+ let persistedEdgeCount = 0;
56
+ for (const payload of payloads) {
57
+ const relations = buildManifestRelations(payload.digest, payload.rawJson);
58
+ for (const descriptor of relations.descriptorRecords) {
59
+ writer.insertManifestDescriptor(descriptor);
60
+ }
61
+ for (const edge of relations.edgeRecords) {
62
+ if (digests.has(edge.parentDigest) && digests.has(edge.childDigest)) {
63
+ writer.insertManifestEdge(edge);
64
+ persistedEdgeCount += 1;
65
+ }
66
+ }
67
+ }
68
+ options.logger.info(`Inserted ${persistedEdgeCount} manifest edges; rebuilding reachability`);
69
+ writer.rebuildManifestReachability();
70
+ options.logger.info("Completed manifest graph processing");
71
+ }
79
72
  function _isMissingManifestError(error) {
80
73
  if (!(error instanceof Error)) {
81
74
  return false;
@@ -95,10 +88,3 @@ async function _getRegistryPullToken(fetchImpl, registryBaseUrl, options, regist
95
88
  registryPullTokenState.token = registryPullToken;
96
89
  return registryPullToken;
97
90
  }
98
- function _enqueueDigest(digest, pendingDigests, queuedDigests, fetchedDigests) {
99
- if (queuedDigests.has(digest) || fetchedDigests.has(digest)) {
100
- return;
101
- }
102
- pendingDigests.push(digest);
103
- queuedDigests.add(digest);
104
- }
@@ -0,0 +1,20 @@
1
+ import type { ManifestKind } from "../../core/index.js";
2
+ interface _RegistryLayer {
3
+ mediaType?: string;
4
+ annotations?: Record<string, unknown>;
5
+ }
6
+ interface _RegistryManifestDocument {
7
+ mediaType?: string;
8
+ artifactType?: string;
9
+ annotations?: Record<string, unknown>;
10
+ config?: {
11
+ mediaType?: string;
12
+ artifactType?: string;
13
+ };
14
+ layers?: _RegistryLayer[];
15
+ subject?: {
16
+ digest?: string;
17
+ };
18
+ }
19
+ export declare function classifyManifestKind(document: _RegistryManifestDocument): ManifestKind | undefined;
20
+ export {};
@@ -0,0 +1,50 @@
1
+ export function classifyManifestKind(document) {
2
+ if (document.mediaType === "application/vnd.oci.image.index.v1+json" ||
3
+ document.mediaType === "application/vnd.docker.distribution.manifest.list.v2+json") {
4
+ return "image_index";
5
+ }
6
+ if (_isSignatureManifest(document)) {
7
+ return "signature_manifest";
8
+ }
9
+ if (_isAttestationManifest(document)) {
10
+ return "attestation_manifest";
11
+ }
12
+ if (document.mediaType === "application/vnd.oci.image.manifest.v1+json") {
13
+ return "image_manifest";
14
+ }
15
+ if (document.mediaType === "application/vnd.oci.artifact.manifest.v1+json") {
16
+ return "artifact_manifest";
17
+ }
18
+ return undefined;
19
+ }
20
+ function _isSignatureManifest(document) {
21
+ const candidates = [
22
+ document.artifactType,
23
+ document.config?.artifactType,
24
+ document.config?.mediaType,
25
+ ...(document.layers?.map((layer) => layer.mediaType) ?? [])
26
+ ];
27
+ if (candidates.some((value) => typeof value === "string" && value.includes("application/vnd.dev.sigstore"))) {
28
+ return true;
29
+ }
30
+ return (typeof document.annotations?.["dev.sigstore.bundle.predicateType"] === "string" &&
31
+ document.annotations["dev.sigstore.bundle.predicateType"] === "https://sigstore.dev/cosign/sign/v1");
32
+ }
33
+ function _isAttestationManifest(document) {
34
+ const candidates = [
35
+ document.artifactType,
36
+ document.config?.artifactType,
37
+ document.config?.mediaType,
38
+ ...(document.layers?.map((layer) => layer.mediaType) ?? [])
39
+ ];
40
+ if (candidates.some((value) => typeof value === "string" && value.includes("application/vnd.in-toto"))) {
41
+ return true;
42
+ }
43
+ if (typeof document.annotations?.["vnd.docker.reference.type"] === "string") {
44
+ return document.annotations["vnd.docker.reference.type"] === "attestation-manifest";
45
+ }
46
+ if (typeof document.annotations?.["dev.sigstore.bundle.predicateType"] === "string") {
47
+ return document.annotations["dev.sigstore.bundle.predicateType"] !== "https://sigstore.dev/cosign/sign/v1";
48
+ }
49
+ return (document.layers?.some((layer) => typeof layer.annotations?.["in-toto.io/predicate-type"] === "string") ?? false);
50
+ }
@@ -0,0 +1,5 @@
1
+ import { type FetchLike, type GitHubScanOptions } from "./_shared.js";
2
+ export interface GitHubPackageMetadata {
3
+ rawJson: string;
4
+ }
5
+ export declare function loadPackageMetadata(fetchImpl: FetchLike, options: GitHubScanOptions): Promise<GitHubPackageMetadata>;
@@ -0,0 +1,45 @@
1
+ import { githubApiBaseUrl, githubApiVersion } from "../../config/index.js";
2
+ import { getOwnerURIComponent } from "../../core/index.js";
3
+ import { buildFetchTransportErrorMessage, buildHttpErrorMessage, withFetchRetry } from "./_shared.js";
4
+ export async function loadPackageMetadata(fetchImpl, options) {
5
+ const ownerURIComponent = await getOwnerURIComponent(fetchImpl, options.owner, options.token, options.logger);
6
+ const url = new URL(`/${ownerURIComponent}/packages/container/${encodeURIComponent(options.packageName)}`, githubApiBaseUrl).toString();
7
+ let response;
8
+ try {
9
+ response = await withFetchRetry(async () => {
10
+ const packageResponse = await fetchImpl(url, {
11
+ headers: {
12
+ Accept: "application/vnd.github+json",
13
+ Authorization: `Bearer ${options.token}`,
14
+ "User-Agent": "ghcr-manager",
15
+ "X-GitHub-Api-Version": githubApiVersion
16
+ }
17
+ });
18
+ if (!packageResponse.ok && _shouldRetryStatus(packageResponse.status)) {
19
+ throw new Error(await buildHttpErrorMessage(packageResponse, "GitHub package metadata request failed"));
20
+ }
21
+ return packageResponse;
22
+ }, {
23
+ logger: options.logger,
24
+ label: "GitHub package metadata request",
25
+ shouldRetry: (error) => _shouldRetryError(error)
26
+ });
27
+ }
28
+ catch (error) {
29
+ throw new Error(buildFetchTransportErrorMessage(error, "GitHub package metadata request failed"), { cause: error });
30
+ }
31
+ if (!response.ok) {
32
+ throw new Error(await buildHttpErrorMessage(response, "GitHub package metadata request failed"));
33
+ }
34
+ const payload = (await response.json());
35
+ return { rawJson: JSON.stringify(payload) };
36
+ }
37
+ function _shouldRetryStatus(status) {
38
+ return status === 429 || status === 502 || status === 503 || status === 504;
39
+ }
40
+ function _shouldRetryError(error) {
41
+ if (!(error instanceof Error)) {
42
+ return false;
43
+ }
44
+ return /fetch failed|status 429|status 502|status 503|status 504/.test(error.message);
45
+ }
@@ -10,4 +10,4 @@ export interface GitHubPackageVersionPageItem {
10
10
  };
11
11
  };
12
12
  }
13
- export declare function loadPackageVersionPage(fetchImpl: FetchLike, githubApiBaseUrl: string, options: GitHubScanOptions, page: number): Promise<GitHubPackageVersionPageItem[]>;
13
+ export declare function loadPackageVersionPage(fetchImpl: FetchLike, options: GitHubScanOptions, page: number): Promise<GitHubPackageVersionPageItem[]>;
@@ -1,7 +1,9 @@
1
+ import { githubApiBaseUrl, githubApiVersion } from "../../config/index.js";
2
+ import { getOwnerURIComponent } from "../../core/index.js";
1
3
  import { buildFetchTransportErrorMessage, buildHttpErrorMessage, withFetchRetry } from "./_shared.js";
2
- export async function loadPackageVersionPage(fetchImpl, githubApiBaseUrl, options, page) {
4
+ export async function loadPackageVersionPage(fetchImpl, options, page) {
3
5
  const startTime = Date.now();
4
- const url = buildPackageVersionPageUrl(githubApiBaseUrl, options, page);
6
+ const url = await buildPackageVersionPageUrl(fetchImpl, options, page);
5
7
  let response;
6
8
  try {
7
9
  response = await withFetchRetry(async () => {
@@ -10,7 +12,7 @@ export async function loadPackageVersionPage(fetchImpl, githubApiBaseUrl, option
10
12
  Accept: "application/vnd.github+json",
11
13
  Authorization: `Bearer ${options.token}`,
12
14
  "User-Agent": "ghcr-manager",
13
- "X-GitHub-Api-Version": "2022-11-28"
15
+ "X-GitHub-Api-Version": githubApiVersion
14
16
  }
15
17
  });
16
18
  if (!pageResponse.ok && _shouldRetryStatus(pageResponse.status)) {
@@ -35,8 +37,9 @@ export async function loadPackageVersionPage(fetchImpl, githubApiBaseUrl, option
35
37
  options.logger.debug(`Loaded GitHub package-version page ${page} in ${Date.now() - startTime}ms (${pageItems.length} items)`);
36
38
  return pageItems;
37
39
  }
38
- function buildPackageVersionPageUrl(githubApiBaseUrl, options, page) {
39
- const url = new URL(`/orgs/${encodeURIComponent(options.owner)}/packages/container/${encodeURIComponent(options.packageName)}/versions`, githubApiBaseUrl);
40
+ async function buildPackageVersionPageUrl(fetchImpl, options, page) {
41
+ const ownerURIComponent = await getOwnerURIComponent(fetchImpl, options.owner, options.token, options.logger);
42
+ const url = new URL(`/${ownerURIComponent}/packages/container/${encodeURIComponent(options.packageName)}/versions`, githubApiBaseUrl);
40
43
  url.searchParams.set("per_page", "100");
41
44
  url.searchParams.set("page", String(page));
42
45
  return url.toString();
@@ -2,7 +2,7 @@ import type { PackageVersionRecord, TagRecord } from "../../core/index.js";
2
2
  import type { ScanWriter } from "../../db/index.js";
3
3
  import { type GitHubPackageVersionPageItem } from "./_package-version-page-load.js";
4
4
  import { type FetchLike, type GitHubScanOptions } from "./_shared.js";
5
- export declare function ingestPackageVersions(fetchImpl: FetchLike, githubApiBaseUrl: string, options: GitHubScanOptions, writer: ScanWriter): Promise<{
5
+ export declare function ingestPackageVersions(fetchImpl: FetchLike, options: GitHubScanOptions, writer: ScanWriter): Promise<{
6
6
  packageVersions: number;
7
7
  tags: number;
8
8
  }>;
@@ -1,17 +1,20 @@
1
1
  import { loadPackageVersionPage } from "./_package-version-page-load.js";
2
2
  import { ingestParallelPaginated } from "./_parallel-paginated-ingest.js";
3
- export async function ingestPackageVersions(fetchImpl, githubApiBaseUrl, options, writer) {
3
+ export async function ingestPackageVersions(fetchImpl, options, writer) {
4
4
  let tagCount = 0;
5
+ const firstPageItems = await loadPackageVersionPage(fetchImpl, options, 1);
5
6
  const result = await ingestParallelPaginated({
6
7
  logger: options.logger,
8
+ firstPageItems,
7
9
  loadPage(page) {
8
- return loadPackageVersionPage(fetchImpl, githubApiBaseUrl, options, page);
10
+ return loadPackageVersionPage(fetchImpl, options, page);
9
11
  },
10
12
  writePage(pageItems) {
11
13
  _writePage(writer, pageItems);
12
14
  tagCount += _countTags(pageItems);
13
15
  }
14
16
  });
17
+ await _assertStableFirstPage(fetchImpl, options, firstPageItems);
15
18
  return { packageVersions: result.items, tags: tagCount };
16
19
  }
17
20
  export function buildTags(packageVersions) {
@@ -24,7 +27,6 @@ export function buildTags(packageVersions) {
24
27
  for (const tagName of tagNames) {
25
28
  tags.push({
26
29
  tag: tagName,
27
- digest: version.digest,
28
30
  versionId: version.versionId
29
31
  });
30
32
  }
@@ -35,7 +37,6 @@ export function normalizePackageVersions(packageVersions) {
35
37
  return packageVersions
36
38
  .map((version) => ({
37
39
  versionId: version.id,
38
- digest: version.name,
39
40
  createdAt: version.created_at,
40
41
  updatedAt: version.updated_at,
41
42
  metadata: version.metadata
@@ -57,3 +58,19 @@ function _writePage(writer, pageItems) {
57
58
  function _countTags(pageItems) {
58
59
  return buildTags(normalizePackageVersions(pageItems)).length;
59
60
  }
61
+ async function _assertStableFirstPage(fetchImpl, options, initialPageItems) {
62
+ const reloadedPageItems = await loadPackageVersionPage(fetchImpl, options, 1);
63
+ if (_buildPageSignature(initialPageItems) === _buildPageSignature(reloadedPageItems)) {
64
+ return;
65
+ }
66
+ throw new Error(`GitHub package-version page 1 changed while scanning ${options.owner}/${options.packageName}; aborting scan`);
67
+ }
68
+ function _buildPageSignature(pageItems) {
69
+ return JSON.stringify(pageItems.map((pageItem) => ({
70
+ id: pageItem.id,
71
+ name: pageItem.name,
72
+ createdAt: pageItem.created_at,
73
+ updatedAt: pageItem.updated_at,
74
+ tags: Array.isArray(pageItem.metadata?.container?.tags) ? [...pageItem.metadata.container.tags] : []
75
+ })));
76
+ }
@@ -3,6 +3,7 @@ export interface ParallelPaginatedIngestOptions<T> {
3
3
  loadPage(page: number): Promise<T[]>;
4
4
  writePage(pageItems: T[], page: number): Promise<void> | void;
5
5
  logger: GitHubScanLogger;
6
+ firstPageItems?: T[];
6
7
  }
7
8
  export interface ParallelPaginatedIngestResult {
8
9
  pages: number;
@@ -1,8 +1,8 @@
1
- import { packageVersionPageFetchConcurrency, paginatedIngestProgressIntervalPages } from "../../tuning/index.js";
1
+ import { packageVersionPageFetchConcurrency, paginatedIngestProgressIntervalPages } from "../../config/index.js";
2
2
  const _DEFAULT_PAGE_SIZE = 100;
3
3
  const _PROGRESS_LABEL = "GitHub package-version pages";
4
4
  export async function ingestParallelPaginated(options) {
5
- const firstPageItems = await options.loadPage(1);
5
+ const firstPageItems = options.firstPageItems ?? (await options.loadPage(1));
6
6
  let pages = 0;
7
7
  let items = 0;
8
8
  let lastLoggedPage = 0;
@@ -1,3 +1,4 @@
1
+ export { buildHttpErrorMessage } from "../../core/index.js";
1
2
  export interface GitHubScanOptions {
2
3
  owner: string;
3
4
  packageName: string;
@@ -20,7 +21,6 @@ export type FetchLike = (input: string, init?: RequestInit) => Promise<FetchLike
20
21
  export declare const acceptedManifestMediaTypes: string;
21
22
  export declare function defaultFetch(input: string, init?: RequestInit): Promise<FetchLikeResponse>;
22
23
  export declare function buildFetchTransportErrorMessage(error: unknown, fallback: string): string;
23
- export declare function buildHttpErrorMessage(response: FetchLikeResponse, fallback: string): Promise<string>;
24
24
  export declare function withFetchRetry<T>(run: () => Promise<T>, options: {
25
25
  logger: GitHubScanLogger;
26
26
  label: string;
@@ -1,4 +1,5 @@
1
- import { ingestRequestRetryCount, ingestRequestRetryDelayMs } from "../../tuning/index.js";
1
+ import { ingestRequestRetryCount, ingestRequestRetryDelayMs } from "../../config/index.js";
2
+ export { buildHttpErrorMessage } from "../../core/index.js";
2
3
  export const acceptedManifestMediaTypes = [
3
4
  "application/vnd.oci.image.index.v1+json",
4
5
  "application/vnd.oci.image.manifest.v1+json",
@@ -14,23 +15,6 @@ export function buildFetchTransportErrorMessage(error, fallback) {
14
15
  details.push(..._collectErrorDetails(error));
15
16
  return details.join(" - ");
16
17
  }
17
- export async function buildHttpErrorMessage(response, fallback) {
18
- const details = [fallback, `status ${response.status}`];
19
- const body = await _readJsonErrorBody(response);
20
- const message = typeof body?.message === "string" ? body.message : undefined;
21
- const documentationUrl = typeof body?.documentation_url === "string" ? body.documentation_url : undefined;
22
- const authenticateHeader = response.headers.get("www-authenticate") ?? undefined;
23
- if (message) {
24
- details.push(message);
25
- }
26
- if (documentationUrl) {
27
- details.push(documentationUrl);
28
- }
29
- if (authenticateHeader) {
30
- details.push(`www-authenticate: ${authenticateHeader}`);
31
- }
32
- return details.join(" - ");
33
- }
34
18
  export async function withFetchRetry(run, options) {
35
19
  let attempt = 0;
36
20
  for (;;) {
@@ -49,22 +33,6 @@ export async function withFetchRetry(run, options) {
49
33
  }
50
34
  }
51
35
  }
52
- async function _readJsonErrorBody(response) {
53
- const contentType = response.headers.get("content-type")?.split(";")[0];
54
- if (contentType && contentType !== "application/json" && !contentType.endsWith("+json")) {
55
- return undefined;
56
- }
57
- try {
58
- const body = await response.json();
59
- if (body && typeof body === "object") {
60
- return body;
61
- }
62
- }
63
- catch {
64
- return undefined;
65
- }
66
- return undefined;
67
- }
68
36
  function _sleep(delayMs) {
69
37
  return new Promise((resolve) => setTimeout(resolve, delayMs));
70
38
  }
@@ -1,7 +1,11 @@
1
1
  import { ScanWriter, SnapshotRepository } from "../../db/index.js";
2
+ import { type GitHubPackageMetadata } from "./_package-metadata-load.js";
2
3
  import { type FetchLike, type GitHubScanOptions } from "./_shared.js";
3
4
  export { type GitHubScanOptions } from "./_shared.js";
5
+ export { loadPackageMetadata, type GitHubPackageMetadata } from "./_package-metadata-load.js";
6
+ export { defaultFetch, type FetchLike, type GitHubScanLogger } from "./_shared.js";
4
7
  interface _GitHubScanRuntime {
5
8
  fetchImpl?: FetchLike;
9
+ packageMetadata?: GitHubPackageMetadata;
6
10
  }
7
11
  export declare function importGitHubScan(options: GitHubScanOptions, writer: ScanWriter, repository: SnapshotRepository, runtime?: _GitHubScanRuntime): Promise<void>;