ghcr-manager 0.9.7 → 0.9.8
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 +39 -1
- package/LICENSE +1 -1
- package/README.md +37 -56
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +7 -10
- package/dist/cleanup-summary/_cleanup-summary.d.ts +3 -4
- package/dist/cleanup-summary/_cleanup-summary.js +2 -3
- package/dist/cli/_cleanup-command.js +1 -1
- package/dist/cli/_tag-selector-resolver.js +29 -9
- package/dist/cli/index.js +0 -4
- package/dist/core/_types.d.ts +1 -6
- package/dist/core/_types.js +1 -1
- package/dist/db/_db-merge-scan-copy.js +3 -2
- package/dist/db/_manifest-reachability.js +47 -8
- package/dist/db/_scan-writer.js +1 -13
- package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
- package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
- package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +9 -0
- package/dist/db/planner/_planner-direct-target-root-tag-filters.js +42 -0
- package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
- package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +198 -0
- package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
- package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
- package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
- package/dist/db/planner/_planner-direct-target-roots-tagged.js +125 -0
- package/dist/db/planner/_planner-direct-target-roots.d.ts +2 -12
- package/dist/db/planner/_planner-direct-target-roots.js +8 -203
- package/dist/db/planner/_planner-direct-target-tags.js +3 -4
- package/dist/db/planner/_planner-output.js +28 -8
- package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +65 -0
- package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +195 -0
- package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
- package/dist/db/planner/_planner-plan-artifacts.js +26 -128
- package/dist/db/planner/_planner-sql.js +13 -2
- package/dist/db/planner/_planner-types.d.ts +2 -0
- package/dist/db/planner/_planner-types.js +1 -0
- package/dist/execute/_plan-executor.js +7 -11
- package/dist/execute/_types.d.ts +2 -19
- package/dist/execute/_untag-client.d.ts +2 -2
- package/dist/execute/_untag-client.js +1 -42
- package/dist/execute/index.d.ts +1 -1
- package/dist/ingest/github/_manifest-kind.d.ts +6 -0
- package/dist/ingest/github/_manifest-kind.js +16 -2
- package/package.json +16 -10
- package/resources/sql/schema/001_schema.sql +14 -4
- package/dist/cli/_untag-command.d.ts +0 -1
- package/dist/cli/_untag-command.js +0 -58
- package/dist/db/_manifest-kind-refinement.d.ts +0 -2
- package/dist/db/_manifest-kind-refinement.js +0 -43
- package/resources/sql/views/002_v_scan_root_manifests.sql +0 -44
- package/resources/sql/views/003_v_digest_tag_relations.sql +0 -50
- package/resources/sql/views/004_v_cleanup_root_closure_members.sql +0 -101
- package/resources/sql/views/005_v_cleanup_blocking_overlaps.sql +0 -42
- package/resources/sql/views/006_v_cleanup_root_decision_readable.sql +0 -67
|
@@ -5,6 +5,7 @@ export const DeletePlanValidationStatuses = {
|
|
|
5
5
|
};
|
|
6
6
|
export const DeletePlanValidationReasonCodes = {
|
|
7
7
|
untagOnlyPartialTagMatch: "untag-only-partial-tag-match",
|
|
8
|
+
untagOnlyRetainedManifest: "untag-only-retained-manifest",
|
|
8
9
|
fullyDeletableNoRetainedOverlap: "fully-deletable-no-retained-overlap",
|
|
9
10
|
blockedOverlapWithRetainedRoot: "blocked-overlap-with-retained-root"
|
|
10
11
|
};
|
|
@@ -2,8 +2,8 @@ import { DeletePlanValidationStatuses } from "../db/index.js";
|
|
|
2
2
|
import { deletePackageVersion } from "./_package-version-delete-client.js";
|
|
3
3
|
import { untagRootTags } from "./_untag-client.js";
|
|
4
4
|
export async function executeDeletePlan(plan, options) {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
let deletedPackageVersionCount = 0;
|
|
6
|
+
let detachedTagCount = 0;
|
|
7
7
|
const directTargetTagSet = new Set(plan.directTargetTags);
|
|
8
8
|
const deletedVersionIds = new Set();
|
|
9
9
|
for (const decision of plan.rootDecisions) {
|
|
@@ -24,7 +24,7 @@ export async function executeDeletePlan(plan, options) {
|
|
|
24
24
|
if (selectedTags.length === 0) {
|
|
25
25
|
throw new Error(`no selected tags resolved for untag-only root ${decision.digest}`);
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
detachedTagCount += await untagRootTags(plan.owner, plan.packageName, decision.versionId, decision.digest, selectedTags, options);
|
|
28
28
|
}
|
|
29
29
|
const closureMembersByRootDigest = new Map(plan.fullyDeletableRoots.map((root) => [
|
|
30
30
|
root.digest,
|
|
@@ -54,10 +54,7 @@ export async function executeDeletePlan(plan, options) {
|
|
|
54
54
|
fetchImpl: options.fetchImpl
|
|
55
55
|
});
|
|
56
56
|
deletedVersionIds.add(target.versionId);
|
|
57
|
-
|
|
58
|
-
versionId: target.versionId,
|
|
59
|
-
digest: target.digest
|
|
60
|
-
});
|
|
57
|
+
deletedPackageVersionCount += 1;
|
|
61
58
|
}
|
|
62
59
|
}
|
|
63
60
|
return {
|
|
@@ -65,9 +62,8 @@ export async function executeDeletePlan(plan, options) {
|
|
|
65
62
|
packageName: plan.packageName,
|
|
66
63
|
scanCompletedAt: plan.scanCompletedAt,
|
|
67
64
|
plannerInputs: plan.plannerInputs,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
blockedRoots: plan.blockedRoots
|
|
71
|
-
unsupportedUntagRoots: []
|
|
65
|
+
deletedPackageVersionCount,
|
|
66
|
+
detachedTagCount,
|
|
67
|
+
blockedRoots: plan.blockedRoots
|
|
72
68
|
};
|
|
73
69
|
}
|
package/dist/execute/_types.d.ts
CHANGED
|
@@ -1,29 +1,12 @@
|
|
|
1
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
2
|
export interface DeleteExecutionSummary {
|
|
19
3
|
owner: string;
|
|
20
4
|
packageName: string;
|
|
21
5
|
scanCompletedAt: string;
|
|
22
6
|
plannerInputs: DeletePlan["plannerInputs"];
|
|
23
|
-
|
|
24
|
-
|
|
7
|
+
deletedPackageVersionCount: number;
|
|
8
|
+
detachedTagCount: number;
|
|
25
9
|
blockedRoots: DeletePlan["blockedRoots"];
|
|
26
|
-
unsupportedUntagRoots: UnsupportedUntagRoot[];
|
|
27
10
|
}
|
|
28
11
|
export interface DeleteExecutionOptions {
|
|
29
12
|
token: string;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { DeleteExecutionOptions
|
|
2
|
-
export declare function untagRootTags(owner: string, packageName: string, sourceVersionId: number, sourceDigest: string, tags: string[], options: DeleteExecutionOptions): Promise<
|
|
1
|
+
import type { DeleteExecutionOptions } from "./_types.js";
|
|
2
|
+
export declare function untagRootTags(owner: string, packageName: string, sourceVersionId: number, sourceDigest: string, tags: string[], options: DeleteExecutionOptions): Promise<number>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { buildDetachedManifestClone } from "./_manifest-detach.js";
|
|
2
2
|
import { findPackageVersionByDigestAndTag } from "./_package-version-page-client.js";
|
|
3
|
-
import { listPackageVersionTagSources, listPresentPackageVersionIds } from "./_package-version-tag-source-client.js";
|
|
4
3
|
import { deletePackageVersion } from "./_package-version-delete-client.js";
|
|
5
4
|
import { loadRegistryManifestByDigest, putRegistryManifestForTag } from "./_registry-manifest-client.js";
|
|
6
5
|
import { loadRegistryPushToken } from "./_registry-token-client.js";
|
|
@@ -11,7 +10,6 @@ export async function untagRootTags(owner, packageName, sourceVersionId, sourceD
|
|
|
11
10
|
const sourceManifest = await loadRegistryManifestByDigest(owner, packageName, sourceDigest, registryToken, options.logger, {
|
|
12
11
|
fetchImpl: options.fetchImpl
|
|
13
12
|
});
|
|
14
|
-
const operations = [];
|
|
15
13
|
for (const tag of tags) {
|
|
16
14
|
options.logger.info(`Detaching tag ${owner}/${packageName}:${tag} from ${sourceDigest}`);
|
|
17
15
|
const detachedManifestJson = buildDetachedManifestClone(sourceManifest.rawJson, sourceManifest.mediaType, {
|
|
@@ -27,45 +25,6 @@ export async function untagRootTags(owner, packageName, sourceVersionId, sourceD
|
|
|
27
25
|
await deletePackageVersion(owner, packageName, detachedVersionId, options.token, options.logger, {
|
|
28
26
|
fetchImpl: options.fetchImpl
|
|
29
27
|
});
|
|
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
28
|
}
|
|
70
|
-
|
|
29
|
+
return tags.length;
|
|
71
30
|
}
|
package/dist/execute/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { executeDeletePlan } from "./_plan-executor.js";
|
|
|
2
2
|
export { listPackageVersionTagSources, listPresentPackageVersionIds } from "./_package-version-tag-source-client.js";
|
|
3
3
|
export { untagRootTags } from "./_untag-client.js";
|
|
4
4
|
export type { GitHubPackageVersionTagSource } from "./_package-version-tag-source-client.js";
|
|
5
|
-
export type { DeleteExecutionSummary
|
|
5
|
+
export type { DeleteExecutionSummary } from "./_types.js";
|
|
@@ -7,6 +7,12 @@ interface _RegistryManifestDocument {
|
|
|
7
7
|
mediaType?: string;
|
|
8
8
|
artifactType?: string;
|
|
9
9
|
annotations?: Record<string, unknown>;
|
|
10
|
+
manifests?: Array<{
|
|
11
|
+
platform?: {
|
|
12
|
+
architecture?: string;
|
|
13
|
+
os?: string;
|
|
14
|
+
};
|
|
15
|
+
}>;
|
|
10
16
|
config?: {
|
|
11
17
|
mediaType?: string;
|
|
12
18
|
artifactType?: string;
|
|
@@ -2,7 +2,7 @@ import { ManifestKinds } from "../../core/index.js";
|
|
|
2
2
|
export function classifyManifestKind(document) {
|
|
3
3
|
if (document.mediaType === "application/vnd.oci.image.index.v1+json" ||
|
|
4
4
|
document.mediaType === "application/vnd.docker.distribution.manifest.list.v2+json") {
|
|
5
|
-
return ManifestKinds.indexManifest;
|
|
5
|
+
return _isMultiArchManifest(document) ? ManifestKinds.multiArchManifest : ManifestKinds.indexManifest;
|
|
6
6
|
}
|
|
7
7
|
if (_isSignatureManifest(document)) {
|
|
8
8
|
return ManifestKinds.signatureManifest;
|
|
@@ -10,7 +10,8 @@ export function classifyManifestKind(document) {
|
|
|
10
10
|
if (_isAttestationManifest(document)) {
|
|
11
11
|
return ManifestKinds.attestationManifest;
|
|
12
12
|
}
|
|
13
|
-
if (document.mediaType === "application/vnd.oci.image.manifest.v1+json"
|
|
13
|
+
if (document.mediaType === "application/vnd.oci.image.manifest.v1+json" ||
|
|
14
|
+
document.mediaType === "application/vnd.docker.distribution.manifest.v2+json") {
|
|
14
15
|
return ManifestKinds.imageManifest;
|
|
15
16
|
}
|
|
16
17
|
if (document.mediaType === "application/vnd.oci.artifact.manifest.v1+json") {
|
|
@@ -18,6 +19,19 @@ export function classifyManifestKind(document) {
|
|
|
18
19
|
}
|
|
19
20
|
return undefined;
|
|
20
21
|
}
|
|
22
|
+
function _isMultiArchManifest(document) {
|
|
23
|
+
const realPlatformDescriptorCount = document.manifests?.filter((descriptor) => {
|
|
24
|
+
const architecture = descriptor.platform?.architecture;
|
|
25
|
+
const os = descriptor.platform?.os;
|
|
26
|
+
return (typeof architecture === "string" &&
|
|
27
|
+
typeof os === "string" &&
|
|
28
|
+
architecture.length > 0 &&
|
|
29
|
+
os.length > 0 &&
|
|
30
|
+
architecture !== "unknown" &&
|
|
31
|
+
os !== "unknown");
|
|
32
|
+
}).length ?? 0;
|
|
33
|
+
return realPlatformDescriptorCount > 1;
|
|
34
|
+
}
|
|
21
35
|
function _isSignatureManifest(document) {
|
|
22
36
|
const candidates = [
|
|
23
37
|
document.artifactType,
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghcr-manager",
|
|
3
3
|
"description": "Inspect, analyze, and manage GitHub Container Registry packages.",
|
|
4
|
-
"homepage": "https://github.com/
|
|
4
|
+
"homepage": "https://github.com/ghcr-manager/ghcr-manager#readme",
|
|
5
5
|
"bugs": {
|
|
6
|
-
"url": "https://github.com/
|
|
6
|
+
"url": "https://github.com/ghcr-manager/ghcr-manager/issues"
|
|
7
7
|
},
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/
|
|
10
|
+
"url": "https://github.com/ghcr-manager/ghcr-manager"
|
|
11
11
|
},
|
|
12
|
-
"version": "0.9.
|
|
12
|
+
"version": "0.9.8",
|
|
13
13
|
"type": "module",
|
|
14
|
+
"workspaces": [
|
|
15
|
+
"visualizer"
|
|
16
|
+
],
|
|
14
17
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
18
|
+
"node": ">=24.0.0"
|
|
16
19
|
},
|
|
17
20
|
"files": [
|
|
18
21
|
"dist",
|
|
@@ -26,19 +29,22 @@
|
|
|
26
29
|
},
|
|
27
30
|
"scripts": {
|
|
28
31
|
"build": "tsc --project tsconfig.json",
|
|
29
|
-
"
|
|
32
|
+
"build:visualizer": "npm run build --workspace visualizer",
|
|
30
33
|
"ghcr-manager": "tsx src/cli/index.ts",
|
|
31
34
|
"ghcr-manager:dist": "node dist/cli/index.js",
|
|
35
|
+
"visualize": "npm run dev --workspace visualizer --",
|
|
36
|
+
"visualizer:start": "npm run start --workspace visualizer --",
|
|
32
37
|
"format": "prettier --write .",
|
|
33
38
|
"format:check": "prettier --check .",
|
|
34
|
-
"lint": "npm run check:file-names && npm run check:test-mapping && npm run typecheck && npm run lint:ts && npm run lint:yaml && npm run lint:markdown && npm run format:check",
|
|
39
|
+
"lint": "npm run check:file-names && npm run check:test-mapping && npm run typecheck && npm run typecheck --workspace visualizer && npm run lint:ts && npm run lint:yaml && npm run lint:markdown && npm run lint --workspace visualizer && npm run format:check",
|
|
35
40
|
"check:file-names": "node tools/tests/check-src-file-names.mjs",
|
|
36
41
|
"check:test-mapping": "node tools/tests/check-test-mapping.mjs",
|
|
37
42
|
"lint:ts": "eslint \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
38
43
|
"lint:yaml": "eslint \".github/**/*.yml\" \"*.yml\"",
|
|
39
44
|
"lint:markdown": "markdownlint-cli2",
|
|
40
45
|
"typecheck": "tsc --noEmit --project tsconfig.json",
|
|
41
|
-
"test": "node --import tsx --test"
|
|
46
|
+
"test": "bash -lc 'shopt -s globstar nullglob; node --import tsx --test tests/**/*.test.ts' && npm run test --workspace visualizer",
|
|
47
|
+
"coverage": "bash -lc 'shopt -s globstar nullglob; c8 --src src --reporter=text --reporter=text-summary node --import tsx --test tests/**/*.test.ts'"
|
|
42
48
|
},
|
|
43
49
|
"dependencies": {
|
|
44
50
|
"better-sqlite3": "^12.9.0"
|
|
@@ -46,10 +52,10 @@
|
|
|
46
52
|
"devDependencies": {
|
|
47
53
|
"@eslint/js": "^10.0.1",
|
|
48
54
|
"@types/better-sqlite3": "^7.6.13",
|
|
49
|
-
"@types/node": "^
|
|
55
|
+
"@types/node": "^24.12.4",
|
|
50
56
|
"c8": "^11.0.0",
|
|
51
57
|
"eslint": "^10.2.1",
|
|
52
|
-
"eslint-plugin-yml": "^3.
|
|
58
|
+
"eslint-plugin-yml": "^3.4.0",
|
|
53
59
|
"globals": "^17.5.0",
|
|
54
60
|
"markdownlint-cli2": "^0.22.1",
|
|
55
61
|
"prettier": "^3.8.3",
|
|
@@ -49,13 +49,10 @@ CREATE TABLE IF NOT EXISTS manifests (
|
|
|
49
49
|
config_media_type TEXT,
|
|
50
50
|
subject_digest TEXT,
|
|
51
51
|
annotations_json TEXT,
|
|
52
|
-
platform_os TEXT,
|
|
53
|
-
platform_architecture TEXT,
|
|
54
|
-
platform_variant TEXT,
|
|
55
52
|
manifest_kind TEXT,
|
|
56
53
|
CHECK(manifest_kind IN (
|
|
57
54
|
'index_manifest',
|
|
58
|
-
'
|
|
55
|
+
'multi_arch_manifest',
|
|
59
56
|
'image_manifest',
|
|
60
57
|
'artifact_manifest',
|
|
61
58
|
'attestation_manifest',
|
|
@@ -109,6 +106,15 @@ CREATE TABLE IF NOT EXISTS manifest_reachability (
|
|
|
109
106
|
CHECK(min_distance >= 0)
|
|
110
107
|
);
|
|
111
108
|
|
|
109
|
+
CREATE TABLE IF NOT EXISTS manifest_graphs (
|
|
110
|
+
scan_id INTEGER NOT NULL,
|
|
111
|
+
digest TEXT NOT NULL,
|
|
112
|
+
graph_id INTEGER NOT NULL,
|
|
113
|
+
PRIMARY KEY(scan_id, digest),
|
|
114
|
+
FOREIGN KEY(scan_id, digest) REFERENCES manifests(scan_id, digest),
|
|
115
|
+
CHECK(graph_id > 0)
|
|
116
|
+
);
|
|
117
|
+
|
|
112
118
|
CREATE TABLE IF NOT EXISTS cleanup_runs (
|
|
113
119
|
cleanup_run_id INTEGER PRIMARY KEY,
|
|
114
120
|
scan_id INTEGER NOT NULL,
|
|
@@ -152,6 +158,7 @@ CREATE TABLE IF NOT EXISTS cleanup_root_decisions (
|
|
|
152
158
|
CHECK(validation_status IN ('fully-deletable', 'blocked', 'untag-only')),
|
|
153
159
|
CHECK(validation_reason_code IN (
|
|
154
160
|
'untag-only-partial-tag-match',
|
|
161
|
+
'untag-only-retained-manifest',
|
|
155
162
|
'fully-deletable-no-retained-overlap',
|
|
156
163
|
'blocked-overlap-with-retained-root'
|
|
157
164
|
)),
|
|
@@ -200,7 +207,10 @@ CREATE INDEX IF NOT EXISTS idx_tags_scan_version ON tags(scan_id, version_id);
|
|
|
200
207
|
CREATE INDEX IF NOT EXISTS idx_manifest_descriptors_scan_child ON manifest_descriptors(scan_id, child_digest);
|
|
201
208
|
CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_parent ON manifest_edges(scan_id, parent_digest);
|
|
202
209
|
CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_child ON manifest_edges(scan_id, child_digest);
|
|
210
|
+
CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_child_kind ON manifest_edges(scan_id, child_digest, edge_kind);
|
|
203
211
|
CREATE INDEX IF NOT EXISTS idx_manifest_reachability_scan_descendant
|
|
204
212
|
ON manifest_reachability(scan_id, descendant_digest);
|
|
205
213
|
CREATE INDEX IF NOT EXISTS idx_manifest_reachability_scan_descendant_distance
|
|
206
214
|
ON manifest_reachability(scan_id, descendant_digest, min_distance);
|
|
215
|
+
CREATE INDEX IF NOT EXISTS idx_manifest_graphs_scan_graph_id
|
|
216
|
+
ON manifest_graphs(scan_id, graph_id);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function handleUntag(args: string[]): Promise<number>;
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { listPackageVersionTagSources, untagRootTags } from "../execute/index.js";
|
|
2
|
-
import { collectRepeatedOption, hasFlag, requireOption, resolveLogLevel, resolveToken } from "./_args.js";
|
|
3
|
-
import { writeJsonOutput } from "./_json-output.js";
|
|
4
|
-
import { createLogger } from "./_logger.js";
|
|
5
|
-
export async function handleUntag(args) {
|
|
6
|
-
const owner = requireOption(args, "--owner");
|
|
7
|
-
const packageName = requireOption(args, "--package");
|
|
8
|
-
const requestedTags = [...new Set(collectRepeatedOption(args, "--tag"))];
|
|
9
|
-
if (requestedTags.length === 0) {
|
|
10
|
-
throw new Error("missing required option: --tag");
|
|
11
|
-
}
|
|
12
|
-
const token = resolveToken(args);
|
|
13
|
-
const dryRun = hasFlag(args, "--dry-run");
|
|
14
|
-
const logger = createLogger(resolveLogLevel(args));
|
|
15
|
-
const tagSources = await listPackageVersionTagSources(owner, packageName, requestedTags, token, logger);
|
|
16
|
-
const matchedTags = new Set(tagSources.map((tagSource) => tagSource.tag));
|
|
17
|
-
const missingTags = requestedTags.filter((tag) => !matchedTags.has(tag));
|
|
18
|
-
if (missingTags.length > 0) {
|
|
19
|
-
throw new Error(`could not resolve tag(s): ${missingTags.join(", ")}`);
|
|
20
|
-
}
|
|
21
|
-
const roots = _groupTagSources(tagSources);
|
|
22
|
-
const untaggedTags = [];
|
|
23
|
-
if (!dryRun) {
|
|
24
|
-
for (const root of roots) {
|
|
25
|
-
untaggedTags.push(...(await untagRootTags(owner, packageName, root.versionId, root.digest, root.tags, {
|
|
26
|
-
token,
|
|
27
|
-
logger
|
|
28
|
-
})));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const summary = {
|
|
32
|
-
owner,
|
|
33
|
-
packageName,
|
|
34
|
-
requestedTags,
|
|
35
|
-
dryRun,
|
|
36
|
-
roots,
|
|
37
|
-
untaggedTags
|
|
38
|
-
};
|
|
39
|
-
writeJsonOutput(args, "--summary-json-path", summary);
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
42
|
-
function _groupTagSources(tagSources) {
|
|
43
|
-
const groups = new Map();
|
|
44
|
-
for (const tagSource of tagSources) {
|
|
45
|
-
const key = `${tagSource.sourceVersionId}:${tagSource.sourceDigest}`;
|
|
46
|
-
const existing = groups.get(key);
|
|
47
|
-
if (existing) {
|
|
48
|
-
existing.tags.push(tagSource.tag);
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
groups.set(key, {
|
|
52
|
-
versionId: tagSource.sourceVersionId,
|
|
53
|
-
digest: tagSource.sourceDigest,
|
|
54
|
-
tags: [tagSource.tag]
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return [...groups.values()];
|
|
58
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { ManifestKinds } from "../core/index.js";
|
|
2
|
-
const _refineManifestKindsStatementByDatabase = new WeakMap();
|
|
3
|
-
export function refineManifestKinds(database, scanId) {
|
|
4
|
-
_refineManifestKindsStatement(database).run(ManifestKinds.crossArchManifest, scanId, ManifestKinds.indexManifest);
|
|
5
|
-
}
|
|
6
|
-
function _refineManifestKindsStatement(database) {
|
|
7
|
-
const cached = _refineManifestKindsStatementByDatabase.get(database);
|
|
8
|
-
if (cached) {
|
|
9
|
-
return cached;
|
|
10
|
-
}
|
|
11
|
-
const statement = database.prepare(`
|
|
12
|
-
UPDATE manifests AS parent
|
|
13
|
-
SET manifest_kind = ?
|
|
14
|
-
WHERE parent.scan_id = ?
|
|
15
|
-
AND parent.manifest_kind = ?
|
|
16
|
-
AND parent.media_type IN (
|
|
17
|
-
'application/vnd.oci.image.index.v1+json',
|
|
18
|
-
'application/vnd.docker.distribution.manifest.list.v2+json'
|
|
19
|
-
)
|
|
20
|
-
AND NOT EXISTS (
|
|
21
|
-
SELECT 1
|
|
22
|
-
FROM tags helper_tag
|
|
23
|
-
WHERE helper_tag.scan_id = parent.scan_id
|
|
24
|
-
AND helper_tag.version_id = parent.version_id
|
|
25
|
-
AND helper_tag.is_digest_tag = 1
|
|
26
|
-
)
|
|
27
|
-
AND EXISTS (
|
|
28
|
-
SELECT 1
|
|
29
|
-
FROM manifest_descriptors descriptor
|
|
30
|
-
JOIN manifests child
|
|
31
|
-
ON child.scan_id = descriptor.scan_id
|
|
32
|
-
AND child.digest = descriptor.child_digest
|
|
33
|
-
WHERE descriptor.scan_id = parent.scan_id
|
|
34
|
-
AND descriptor.parent_digest = parent.digest
|
|
35
|
-
AND child.media_type IN (
|
|
36
|
-
'application/vnd.oci.image.manifest.v1+json',
|
|
37
|
-
'application/vnd.docker.distribution.manifest.v2+json'
|
|
38
|
-
)
|
|
39
|
-
)
|
|
40
|
-
`);
|
|
41
|
-
_refineManifestKindsStatementByDatabase.set(database, statement);
|
|
42
|
-
return statement;
|
|
43
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
DROP VIEW IF EXISTS v_scan_root_manifests;
|
|
2
|
-
|
|
3
|
-
CREATE VIEW v_scan_root_manifests AS
|
|
4
|
-
SELECT
|
|
5
|
-
ps.scan_id,
|
|
6
|
-
ps.owner,
|
|
7
|
-
ps.package_name,
|
|
8
|
-
m.version_id AS root_version_id,
|
|
9
|
-
m.digest AS root_digest,
|
|
10
|
-
m.manifest_kind AS root_manifest_kind,
|
|
11
|
-
pv.created_at,
|
|
12
|
-
pv.updated_at,
|
|
13
|
-
COUNT(t.tag) AS tag_count,
|
|
14
|
-
CASE WHEN COUNT(t.tag) > 0 THEN 1 ELSE 0 END AS is_tagged,
|
|
15
|
-
CASE
|
|
16
|
-
WHEN EXISTS (
|
|
17
|
-
SELECT 1
|
|
18
|
-
FROM manifest_reachability mr
|
|
19
|
-
WHERE mr.scan_id = m.scan_id
|
|
20
|
-
AND mr.descendant_digest = m.digest
|
|
21
|
-
AND mr.min_distance > 0
|
|
22
|
-
) THEN 1
|
|
23
|
-
ELSE 0
|
|
24
|
-
END AS has_ancestor
|
|
25
|
-
FROM manifests m
|
|
26
|
-
JOIN package_versions pv
|
|
27
|
-
ON pv.scan_id = m.scan_id
|
|
28
|
-
AND pv.version_id = m.version_id
|
|
29
|
-
JOIN package_scans ps
|
|
30
|
-
ON ps.scan_id = m.scan_id
|
|
31
|
-
LEFT JOIN tags t
|
|
32
|
-
ON t.scan_id = m.scan_id
|
|
33
|
-
AND t.version_id = m.version_id
|
|
34
|
-
AND t.is_digest_tag = 0
|
|
35
|
-
GROUP BY
|
|
36
|
-
ps.scan_id,
|
|
37
|
-
ps.owner,
|
|
38
|
-
ps.package_name,
|
|
39
|
-
m.version_id,
|
|
40
|
-
m.digest,
|
|
41
|
-
m.manifest_kind,
|
|
42
|
-
pv.created_at,
|
|
43
|
-
pv.updated_at
|
|
44
|
-
;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
DROP VIEW IF EXISTS v_digest_tag_relations;
|
|
2
|
-
|
|
3
|
-
CREATE VIEW v_digest_tag_relations AS
|
|
4
|
-
WITH digest_like_tags AS (
|
|
5
|
-
SELECT
|
|
6
|
-
lsp.scan_id,
|
|
7
|
-
lsp.owner,
|
|
8
|
-
lsp.package_name,
|
|
9
|
-
t.tag,
|
|
10
|
-
t.version_id AS artifact_version_id,
|
|
11
|
-
m.digest AS artifact_digest,
|
|
12
|
-
m.manifest_kind AS artifact_manifest_kind,
|
|
13
|
-
m.subject_digest AS artifact_subject_digest,
|
|
14
|
-
'sha256:' || SUBSTR(t.tag, 8, 64) AS parent_digest,
|
|
15
|
-
SUBSTR(t.tag, 72) AS tag_suffix
|
|
16
|
-
FROM tags t
|
|
17
|
-
JOIN v_latest_scan_per_package lsp
|
|
18
|
-
ON lsp.scan_id = t.scan_id
|
|
19
|
-
JOIN manifests m
|
|
20
|
-
ON m.scan_id = t.scan_id
|
|
21
|
-
AND m.version_id = t.version_id
|
|
22
|
-
WHERE t.tag LIKE 'sha256-%'
|
|
23
|
-
AND LENGTH(t.tag) >= 71
|
|
24
|
-
AND SUBSTR(t.tag, 8, 64) NOT GLOB '*[^0-9A-Fa-f]*'
|
|
25
|
-
)
|
|
26
|
-
SELECT
|
|
27
|
-
dlt.scan_id,
|
|
28
|
-
dlt.owner,
|
|
29
|
-
dlt.package_name,
|
|
30
|
-
dlt.tag,
|
|
31
|
-
dlt.artifact_version_id,
|
|
32
|
-
dlt.artifact_digest,
|
|
33
|
-
dlt.artifact_manifest_kind,
|
|
34
|
-
dlt.artifact_subject_digest,
|
|
35
|
-
dlt.parent_digest,
|
|
36
|
-
dlt.tag_suffix,
|
|
37
|
-
pm.version_id AS parent_version_id,
|
|
38
|
-
pm.manifest_kind AS parent_manifest_kind,
|
|
39
|
-
CASE
|
|
40
|
-
WHEN dlt.artifact_subject_digest = dlt.parent_digest THEN 1
|
|
41
|
-
ELSE 0
|
|
42
|
-
END AS subject_matches_parent,
|
|
43
|
-
CASE
|
|
44
|
-
WHEN pm.digest IS NULL THEN 0
|
|
45
|
-
ELSE 1
|
|
46
|
-
END AS parent_exists
|
|
47
|
-
FROM digest_like_tags dlt
|
|
48
|
-
LEFT JOIN manifests pm
|
|
49
|
-
ON pm.scan_id = dlt.scan_id
|
|
50
|
-
AND pm.digest = dlt.parent_digest;
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
DROP VIEW IF EXISTS v_cleanup_root_closure_members;
|
|
2
|
-
|
|
3
|
-
CREATE VIEW v_cleanup_root_closure_members AS
|
|
4
|
-
WITH selected_roots AS (
|
|
5
|
-
SELECT
|
|
6
|
-
run.cleanup_run_id,
|
|
7
|
-
run.scan_id,
|
|
8
|
-
ps.owner,
|
|
9
|
-
ps.package_name,
|
|
10
|
-
decision.digest AS root_digest,
|
|
11
|
-
root_manifest.version_id AS root_version_id,
|
|
12
|
-
root_manifest.manifest_kind AS root_manifest_kind,
|
|
13
|
-
decision.selection_mode,
|
|
14
|
-
decision.selection_reason,
|
|
15
|
-
decision.validation_status,
|
|
16
|
-
decision.validation_reason_code,
|
|
17
|
-
decision.validation_reason
|
|
18
|
-
FROM cleanup_root_decisions decision
|
|
19
|
-
JOIN cleanup_runs run
|
|
20
|
-
ON run.cleanup_run_id = decision.cleanup_run_id
|
|
21
|
-
AND run.scan_id = decision.scan_id
|
|
22
|
-
JOIN package_scans ps
|
|
23
|
-
ON ps.scan_id = run.scan_id
|
|
24
|
-
JOIN manifests root_manifest
|
|
25
|
-
ON root_manifest.scan_id = decision.scan_id
|
|
26
|
-
AND root_manifest.digest = decision.digest
|
|
27
|
-
),
|
|
28
|
-
closure_members AS (
|
|
29
|
-
SELECT
|
|
30
|
-
sr.cleanup_run_id,
|
|
31
|
-
sr.scan_id,
|
|
32
|
-
sr.owner,
|
|
33
|
-
sr.package_name,
|
|
34
|
-
sr.root_digest,
|
|
35
|
-
sr.root_version_id,
|
|
36
|
-
sr.root_manifest_kind,
|
|
37
|
-
sr.selection_mode,
|
|
38
|
-
sr.selection_reason,
|
|
39
|
-
sr.validation_status,
|
|
40
|
-
sr.validation_reason_code,
|
|
41
|
-
sr.validation_reason,
|
|
42
|
-
sr.root_digest AS member_digest,
|
|
43
|
-
sr.root_version_id AS member_version_id,
|
|
44
|
-
sr.root_manifest_kind AS member_manifest_kind,
|
|
45
|
-
0 AS hops_from_root,
|
|
46
|
-
'root' AS member_role
|
|
47
|
-
FROM selected_roots sr
|
|
48
|
-
|
|
49
|
-
UNION ALL
|
|
50
|
-
|
|
51
|
-
SELECT
|
|
52
|
-
sr.cleanup_run_id,
|
|
53
|
-
sr.scan_id,
|
|
54
|
-
sr.owner,
|
|
55
|
-
sr.package_name,
|
|
56
|
-
sr.root_digest,
|
|
57
|
-
sr.root_version_id,
|
|
58
|
-
sr.root_manifest_kind,
|
|
59
|
-
sr.selection_mode,
|
|
60
|
-
sr.selection_reason,
|
|
61
|
-
sr.validation_status,
|
|
62
|
-
sr.validation_reason_code,
|
|
63
|
-
sr.validation_reason,
|
|
64
|
-
member_manifest.digest AS member_digest,
|
|
65
|
-
member_manifest.version_id AS member_version_id,
|
|
66
|
-
member_manifest.manifest_kind AS member_manifest_kind,
|
|
67
|
-
reachability.min_distance AS hops_from_root,
|
|
68
|
-
'descendant' AS member_role
|
|
69
|
-
FROM selected_roots sr
|
|
70
|
-
JOIN manifest_reachability reachability
|
|
71
|
-
ON reachability.scan_id = sr.scan_id
|
|
72
|
-
AND reachability.ancestor_digest = sr.root_digest
|
|
73
|
-
AND reachability.min_distance > 0
|
|
74
|
-
JOIN manifests member_manifest
|
|
75
|
-
ON member_manifest.scan_id = sr.scan_id
|
|
76
|
-
AND member_manifest.digest = reachability.descendant_digest
|
|
77
|
-
)
|
|
78
|
-
SELECT
|
|
79
|
-
cm.cleanup_run_id,
|
|
80
|
-
cm.scan_id,
|
|
81
|
-
cm.owner,
|
|
82
|
-
cm.package_name,
|
|
83
|
-
cm.root_digest,
|
|
84
|
-
cm.root_version_id,
|
|
85
|
-
cm.root_manifest_kind,
|
|
86
|
-
cm.selection_mode,
|
|
87
|
-
cm.selection_reason,
|
|
88
|
-
cm.validation_status,
|
|
89
|
-
cm.validation_reason_code,
|
|
90
|
-
cm.validation_reason,
|
|
91
|
-
cm.member_digest,
|
|
92
|
-
cm.member_version_id,
|
|
93
|
-
cm.member_manifest_kind,
|
|
94
|
-
cm.hops_from_root,
|
|
95
|
-
cm.member_role,
|
|
96
|
-
tag.tag AS member_tag
|
|
97
|
-
FROM closure_members cm
|
|
98
|
-
LEFT JOIN tags tag
|
|
99
|
-
ON tag.scan_id = cm.scan_id
|
|
100
|
-
AND tag.version_id = cm.member_version_id
|
|
101
|
-
AND tag.is_digest_tag = 0;
|