ghcr-cleanup-manager 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +356 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +5 -0
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +224 -0
- package/dist/cleanup-summary/_cleanup-summary.d.ts +56 -0
- package/dist/cleanup-summary/_cleanup-summary.js +59 -0
- package/dist/cleanup-summary/index.d.ts +2 -0
- package/dist/cleanup-summary/index.js +2 -0
- package/dist/cli/_args.d.ts +7 -0
- package/dist/cli/_args.js +41 -0
- package/dist/cli/_cleanup-command.d.ts +1 -0
- package/dist/cli/_cleanup-command.js +142 -0
- package/dist/cli/_db-merge-command.d.ts +1 -0
- package/dist/cli/_db-merge-command.js +41 -0
- package/dist/cli/_github-output.d.ts +10 -0
- package/dist/cli/_github-output.js +13 -0
- package/dist/cli/_json-output.d.ts +1 -0
- package/dist/cli/_json-output.js +11 -0
- package/dist/cli/_logger.d.ts +10 -0
- package/dist/cli/_logger.js +26 -0
- package/dist/cli/_older-than.d.ts +5 -0
- package/dist/cli/_older-than.js +42 -0
- package/dist/cli/_planner-options.d.ts +20 -0
- package/dist/cli/_planner-options.js +83 -0
- package/dist/cli/_scan-command.d.ts +1 -0
- package/dist/cli/_scan-command.js +39 -0
- package/dist/cli/_tag-selector-resolver.d.ts +3 -0
- package/dist/cli/_tag-selector-resolver.js +132 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +52 -0
- package/dist/config/_service-constants.d.ts +3 -0
- package/dist/config/_service-constants.js +3 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +9 -0
- package/dist/core/_digest-tag.d.ts +2 -0
- package/dist/core/_digest-tag.js +12 -0
- package/dist/core/_github-package-owner.d.ts +11 -0
- package/dist/core/_github-package-owner.js +45 -0
- package/dist/core/_http-error.d.ts +6 -0
- package/dist/core/_http-error.js +33 -0
- package/dist/core/_types.d.ts +54 -0
- package/dist/core/_types.js +8 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +4 -0
- package/dist/db/_cleanup-run-writer.d.ts +10 -0
- package/dist/db/_cleanup-run-writer.js +113 -0
- package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
- package/dist/db/_db-merge-cleanup-copy.js +172 -0
- package/dist/db/_db-merge-history.d.ts +2 -0
- package/dist/db/_db-merge-history.js +15 -0
- package/dist/db/_db-merge-repository.d.ts +8 -0
- package/dist/db/_db-merge-repository.js +95 -0
- package/dist/db/_db-merge-scan-copy.d.ts +10 -0
- package/dist/db/_db-merge-scan-copy.js +70 -0
- package/dist/db/_db-merge-types.d.ts +44 -0
- package/dist/db/_db-merge-types.js +1 -0
- package/dist/db/_github-actions-run-url.d.ts +1 -0
- package/dist/db/_github-actions-run-url.js +9 -0
- package/dist/db/_manifest-reachability.d.ts +2 -0
- package/dist/db/_manifest-reachability.js +158 -0
- package/dist/db/_scan-writer.d.ts +20 -0
- package/dist/db/_scan-writer.js +184 -0
- package/dist/db/_schema.d.ts +2 -0
- package/dist/db/_schema.js +19 -0
- package/dist/db/_snapshot-repository.d.ts +24 -0
- package/dist/db/_snapshot-repository.js +86 -0
- package/dist/db/_sql-placeholders.d.ts +2 -0
- package/dist/db/_sql-placeholders.js +16 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +13 -0
- package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
- package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
- package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +7 -0
- package/dist/db/planner/_planner-direct-target-root-tag-filters.js +41 -0
- package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
- package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +189 -0
- package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
- package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
- package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
- package/dist/db/planner/_planner-direct-target-roots-tagged.js +117 -0
- package/dist/db/planner/_planner-direct-target-roots.d.ts +8 -0
- package/dist/db/planner/_planner-direct-target-roots.js +17 -0
- package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
- package/dist/db/planner/_planner-direct-target-tags.js +54 -0
- package/dist/db/planner/_planner-latest-scan.d.ts +10 -0
- package/dist/db/planner/_planner-latest-scan.js +20 -0
- package/dist/db/planner/_planner-output.d.ts +5 -0
- package/dist/db/planner/_planner-output.js +111 -0
- package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +110 -0
- package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +188 -0
- package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
- package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
- package/dist/db/planner/_planner-plan-artifacts.js +109 -0
- package/dist/db/planner/_planner-repository.d.ts +44 -0
- package/dist/db/planner/_planner-repository.js +105 -0
- package/dist/db/planner/_planner-sql.d.ts +12 -0
- package/dist/db/planner/_planner-sql.js +46 -0
- package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
- package/dist/db/planner/_planner-tag-selectors.js +57 -0
- package/dist/db/planner/_planner-types.d.ts +144 -0
- package/dist/db/planner/_planner-types.js +49 -0
- package/dist/db/planner/index.d.ts +3 -0
- package/dist/db/planner/index.js +2 -0
- package/dist/execute/_http.d.ts +7 -0
- package/dist/execute/_http.js +48 -0
- package/dist/execute/_manifest-detach.d.ts +4 -0
- package/dist/execute/_manifest-detach.js +31 -0
- package/dist/execute/_package-version-delete-client.d.ts +4 -0
- package/dist/execute/_package-version-delete-client.js +34 -0
- package/dist/execute/_package-version-page-client.d.ts +14 -0
- package/dist/execute/_package-version-page-client.js +64 -0
- package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
- package/dist/execute/_package-version-tag-source-client.js +65 -0
- package/dist/execute/_plan-executor.d.ts +3 -0
- package/dist/execute/_plan-executor.js +69 -0
- package/dist/execute/_registry-manifest-client.d.ts +12 -0
- package/dist/execute/_registry-manifest-client.js +79 -0
- package/dist/execute/_registry-token-client.d.ts +4 -0
- package/dist/execute/_registry-token-client.js +37 -0
- package/dist/execute/_types.d.ts +34 -0
- package/dist/execute/_types.js +1 -0
- package/dist/execute/_untag-client.d.ts +2 -0
- package/dist/execute/_untag-client.js +30 -0
- package/dist/execute/index.d.ts +5 -0
- package/dist/execute/index.js +3 -0
- package/dist/ingest/github/_manifest-client.d.ts +14 -0
- package/dist/ingest/github/_manifest-client.js +108 -0
- package/dist/ingest/github/_manifest-ingest.d.ts +3 -0
- package/dist/ingest/github/_manifest-ingest.js +90 -0
- package/dist/ingest/github/_manifest-kind.d.ts +26 -0
- package/dist/ingest/github/_manifest-kind.js +65 -0
- package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
- package/dist/ingest/github/_package-metadata-load.js +45 -0
- package/dist/ingest/github/_package-version-page-load.d.ts +13 -0
- package/dist/ingest/github/_package-version-page-load.js +55 -0
- package/dist/ingest/github/_packages-client.d.ts +10 -0
- package/dist/ingest/github/_packages-client.js +76 -0
- package/dist/ingest/github/_parallel-paginated-ingest.d.ts +12 -0
- package/dist/ingest/github/_parallel-paginated-ingest.js +49 -0
- package/dist/ingest/github/_registry-token-client.d.ts +6 -0
- package/dist/ingest/github/_registry-token-client.js +67 -0
- package/dist/ingest/github/_shared.d.ts +28 -0
- package/dist/ingest/github/_shared.js +70 -0
- package/dist/ingest/github/index.d.ts +11 -0
- package/dist/ingest/github/index.js +29 -0
- package/package.json +78 -0
- package/resources/sql/schema/001_schema.sql +222 -0
- package/resources/sql/views/001_v_latest_scan_per_package.sql +27 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { ManifestKinds } from "../core/index.js";
|
|
2
|
+
import { DeletePlanValidationStatuses } from "../db/index.js";
|
|
3
|
+
const _DEFAULT_MAX_DIRECT_TARGET_TAGS = 100;
|
|
4
|
+
const _DEFAULT_MAX_ROOTS_PER_SECTION = 100;
|
|
5
|
+
const _DEFAULT_MAX_TAG_TEXT_LENGTH = 40;
|
|
6
|
+
export function renderCleanupSummaryMarkdown(summary, options) {
|
|
7
|
+
const maxDirectTargetTags = options.maxDirectTargetTags ?? _DEFAULT_MAX_DIRECT_TARGET_TAGS;
|
|
8
|
+
const maxRootsPerSection = options.maxRootsPerSection ?? _DEFAULT_MAX_ROOTS_PER_SECTION;
|
|
9
|
+
const lines = [
|
|
10
|
+
"## Cleanup Summary",
|
|
11
|
+
"",
|
|
12
|
+
"| Field | Value |",
|
|
13
|
+
"| --- | --- |",
|
|
14
|
+
`| 📦 Package | \`${_escapeInlineCode(`${summary.owner}/${summary.packageName}`)}\` |`,
|
|
15
|
+
`| ⚙️ Mode | ${summary.dryRun ? "Cleanup dry-run" : "Cleanup"} |`,
|
|
16
|
+
`| 🏷️ Selected tags | ${summary.directTargetTags.length} |`,
|
|
17
|
+
`| 🔖 Deleted tags | ${summary.changes.deletedTags} |`,
|
|
18
|
+
`| 🖼️ Deleted images | ${summary.changes.deletedImages} |`,
|
|
19
|
+
`| 📚 Deleted multi-arch manifests | ${summary.changes.deletedMultiArchManifests} |`,
|
|
20
|
+
`| 🗂️ Deleted indexes | ${summary.changes.deletedIndexes} |`,
|
|
21
|
+
`| 📄 Deleted total | ${summary.changes.deletedTotal} |`,
|
|
22
|
+
`| 🔗 Tag-only updates | ${summary.untagOnlyRoots.length} |`,
|
|
23
|
+
`| 🛡️ Blocked items | ${summary.blockedRoots.length} |`,
|
|
24
|
+
""
|
|
25
|
+
];
|
|
26
|
+
lines.push(..._renderPlannedDeleteBreakdown(summary));
|
|
27
|
+
lines.push(..._renderPlannerInputs(summary.plannerInputs));
|
|
28
|
+
lines.push(..._renderDirectTargetTags(summary.directTargetTags, maxDirectTargetTags));
|
|
29
|
+
lines.push(..._renderRootSection("🗑️ Deleted items", summary.fullyDeletableRoots, maxRootsPerSection));
|
|
30
|
+
lines.push(..._renderRootSection("🔗 Tags removed only", summary.untagOnlyRoots, maxRootsPerSection));
|
|
31
|
+
lines.push(..._renderRootSection("🛡️ Blocked items", summary.blockedRoots, maxRootsPerSection));
|
|
32
|
+
if (!summary.dryRun && (summary.deletedPackageVersionCount > 0 || summary.detachedTagCount > 0)) {
|
|
33
|
+
lines.push(..._renderLiveEffects(summary));
|
|
34
|
+
}
|
|
35
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
36
|
+
}
|
|
37
|
+
function _renderPlannedDeleteBreakdown(summary) {
|
|
38
|
+
const rows = [
|
|
39
|
+
{ label: "Images", count: summary.changes.deletedImages },
|
|
40
|
+
{ label: "Multi-arch manifests", count: summary.changes.deletedMultiArchManifests },
|
|
41
|
+
{ label: "Artifact manifests", count: summary.changes.deletedArtifactManifests },
|
|
42
|
+
{ label: "Signatures", count: summary.changes.deletedSignatures },
|
|
43
|
+
{ label: "Attestations", count: summary.changes.deletedAttestations },
|
|
44
|
+
{ label: "Generic indexes", count: summary.changes.deletedIndexes }
|
|
45
|
+
].filter((row) => row.count > 0);
|
|
46
|
+
if (rows.length === 0) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return [
|
|
50
|
+
"<details>",
|
|
51
|
+
"<summary>📦 Deleted item breakdown</summary>",
|
|
52
|
+
"",
|
|
53
|
+
"| Type | Count |",
|
|
54
|
+
"| --- | --- |",
|
|
55
|
+
...rows.map((row) => `| ${row.label} | ${row.count} |`),
|
|
56
|
+
"",
|
|
57
|
+
"</details>",
|
|
58
|
+
""
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
function _renderPlannerInputs(plannerInputs) {
|
|
62
|
+
const rows = _getPlannerInputRows(plannerInputs);
|
|
63
|
+
const patternLines = _getPlannerPatternLines(plannerInputs);
|
|
64
|
+
return [
|
|
65
|
+
"<details>",
|
|
66
|
+
"<summary>⚙️ Cleanup filter</summary>",
|
|
67
|
+
"",
|
|
68
|
+
"| Filter | Value |",
|
|
69
|
+
"| --- | --- |",
|
|
70
|
+
...(rows.length > 0 ? rows : ["| (none) | No cleanup filters recorded |"]),
|
|
71
|
+
...(patternLines.length > 0 ? ["", ...patternLines] : []),
|
|
72
|
+
"",
|
|
73
|
+
"</details>",
|
|
74
|
+
""
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
function _renderDirectTargetTags(tags, maxDirectTargetTags) {
|
|
78
|
+
if (tags.length === 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const visibleTags = tags.slice(0, maxDirectTargetTags).map((tag) => `- \`${_escapeInlineCode(tag)}\``);
|
|
82
|
+
const lines = ["<details>", "<summary>🏷️ Selected tags</summary>", "", ...visibleTags];
|
|
83
|
+
if (tags.length > maxDirectTargetTags) {
|
|
84
|
+
lines.push("", `_Showing first ${maxDirectTargetTags} of ${tags.length} selected tags._`);
|
|
85
|
+
}
|
|
86
|
+
lines.push("", "</details>", "");
|
|
87
|
+
return lines;
|
|
88
|
+
}
|
|
89
|
+
function _renderRootSection(title, roots, maxRootsPerSection) {
|
|
90
|
+
if (roots.length === 0) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const lines = ["<details>", `<summary>${title}</summary>`, ""];
|
|
94
|
+
lines.push("| Version | Type | Digest | Tags | Outcome |");
|
|
95
|
+
lines.push("| --- | --- | --- | --- | --- |");
|
|
96
|
+
for (const root of roots.slice(0, maxRootsPerSection)) {
|
|
97
|
+
lines.push(`| ${root.versionId} | ${_escapeMarkdown(_describeManifestKind(root.manifestKind))} | \`${_escapeInlineCode(_shortDigest(root.digest))}\` | ${_escapeMarkdown(_formatTags(root))} | ${_escapeMarkdown(_formatReason(root))} |`);
|
|
98
|
+
}
|
|
99
|
+
lines.push("", "_Tag lists may be truncated for table width._");
|
|
100
|
+
if (roots.length > maxRootsPerSection) {
|
|
101
|
+
lines.push("", `_Showing first ${maxRootsPerSection} of ${roots.length} ${title.toLowerCase()}._`);
|
|
102
|
+
}
|
|
103
|
+
lines.push("", "</details>", "");
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
|
106
|
+
function _renderLiveEffects(summary) {
|
|
107
|
+
const lines = ["### Applied changes", ""];
|
|
108
|
+
lines.push(`- Deleted package versions: ${summary.deletedPackageVersionCount}`);
|
|
109
|
+
lines.push(`- Detached tags: ${summary.detachedTagCount}`);
|
|
110
|
+
lines.push("");
|
|
111
|
+
return lines;
|
|
112
|
+
}
|
|
113
|
+
function _formatTags(root) {
|
|
114
|
+
const tags = root.rootTags.length > 0 ? root.rootTags : root.matchedTags;
|
|
115
|
+
if (tags.length === 0) {
|
|
116
|
+
return "(untagged)";
|
|
117
|
+
}
|
|
118
|
+
const joinedTags = tags.join(", ");
|
|
119
|
+
if (joinedTags.length <= _DEFAULT_MAX_TAG_TEXT_LENGTH) {
|
|
120
|
+
return joinedTags;
|
|
121
|
+
}
|
|
122
|
+
return `${joinedTags.slice(0, _DEFAULT_MAX_TAG_TEXT_LENGTH - 3)}...`;
|
|
123
|
+
}
|
|
124
|
+
function _formatReason(root) {
|
|
125
|
+
if (root.validationStatus === DeletePlanValidationStatuses.blocked) {
|
|
126
|
+
const blocking = root.blockingDigest ? _shortDigest(root.blockingDigest) : "another item";
|
|
127
|
+
const overlap = root.overlapDigest ? ` via ${_shortDigest(root.overlapDigest)}` : "";
|
|
128
|
+
return `Blocked by retained item ${blocking}${overlap}`;
|
|
129
|
+
}
|
|
130
|
+
if (root.validationStatus === DeletePlanValidationStatuses.untagOnly) {
|
|
131
|
+
return "Remove selected tags, keep item";
|
|
132
|
+
}
|
|
133
|
+
return "Delete item and descendants";
|
|
134
|
+
}
|
|
135
|
+
function _shortDigest(value) {
|
|
136
|
+
if (!value.startsWith("sha256:") || value.length <= 20) {
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
return `${value.slice(0, 15)}...${value.slice(-8)}`;
|
|
140
|
+
}
|
|
141
|
+
function _escapeInlineCode(value) {
|
|
142
|
+
return value.replaceAll("`", "\\`");
|
|
143
|
+
}
|
|
144
|
+
function _escapeMarkdown(value) {
|
|
145
|
+
return value.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
146
|
+
}
|
|
147
|
+
function _getPlannerInputRows(plannerInputs) {
|
|
148
|
+
const rows = [];
|
|
149
|
+
for (const [key, value] of Object.entries(plannerInputs)) {
|
|
150
|
+
rows.push(`| ${_escapeMarkdown(_plannerInputLabel(key))} | ${_escapeMarkdown(_formatPlannerInputValue(value))} |`);
|
|
151
|
+
}
|
|
152
|
+
return rows;
|
|
153
|
+
}
|
|
154
|
+
function _plannerInputLabel(key) {
|
|
155
|
+
switch (key) {
|
|
156
|
+
case "deleteTags":
|
|
157
|
+
return "Delete tags";
|
|
158
|
+
case "excludeTags":
|
|
159
|
+
return "Exclude tags";
|
|
160
|
+
case "useRegex":
|
|
161
|
+
return "Use regex";
|
|
162
|
+
case "deleteUntagged":
|
|
163
|
+
return "Delete untagged";
|
|
164
|
+
case "keepNTagged":
|
|
165
|
+
return "Keep newest tagged";
|
|
166
|
+
case "keepNUntagged":
|
|
167
|
+
return "Keep newest untagged";
|
|
168
|
+
case "olderThan":
|
|
169
|
+
return "Older than";
|
|
170
|
+
case "cutoffTimestamp":
|
|
171
|
+
return "Cutoff timestamp";
|
|
172
|
+
case "deleteGhostImages":
|
|
173
|
+
return "Delete ghost images";
|
|
174
|
+
case "deletePartialImages":
|
|
175
|
+
return "Delete partial images";
|
|
176
|
+
case "deleteOrphanedImages":
|
|
177
|
+
return "Delete orphaned images";
|
|
178
|
+
default:
|
|
179
|
+
return key;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function _formatPlannerInputValue(value) {
|
|
183
|
+
if (Array.isArray(value)) {
|
|
184
|
+
if (value.length === 0) {
|
|
185
|
+
return "(none)";
|
|
186
|
+
}
|
|
187
|
+
return value.length === 1 ? "1 pattern" : `${value.length} patterns`;
|
|
188
|
+
}
|
|
189
|
+
if (typeof value === "boolean") {
|
|
190
|
+
return value ? "yes" : "no";
|
|
191
|
+
}
|
|
192
|
+
return String(value);
|
|
193
|
+
}
|
|
194
|
+
function _getPlannerPatternLines(plannerInputs) {
|
|
195
|
+
const lines = [];
|
|
196
|
+
for (const [key, value] of Object.entries(plannerInputs)) {
|
|
197
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
lines.push(`- ${_plannerInputLabel(key)}:`);
|
|
201
|
+
for (const item of value) {
|
|
202
|
+
lines.push(` - \`${_escapeInlineCode(String(item))}\``);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return lines;
|
|
206
|
+
}
|
|
207
|
+
function _describeManifestKind(manifestKind) {
|
|
208
|
+
switch (manifestKind) {
|
|
209
|
+
case ManifestKinds.imageManifest:
|
|
210
|
+
return "image";
|
|
211
|
+
case ManifestKinds.multiArchManifest:
|
|
212
|
+
return "multi-arch";
|
|
213
|
+
case ManifestKinds.indexManifest:
|
|
214
|
+
return "index";
|
|
215
|
+
case ManifestKinds.signatureManifest:
|
|
216
|
+
return "signature";
|
|
217
|
+
case ManifestKinds.attestationManifest:
|
|
218
|
+
return "attestation";
|
|
219
|
+
case ManifestKinds.artifactManifest:
|
|
220
|
+
return "artifact";
|
|
221
|
+
default:
|
|
222
|
+
return "item";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ManifestKind } from "../core/index.js";
|
|
2
|
+
import type { DeletePlan, DeletePlanSelectionMode, DeletePlanSelectionReason, DeletePlanValidationReasonCode, DeletePlanValidationStatus } from "../db/index.js";
|
|
3
|
+
import type { DeleteExecutionSummary } from "../execute/index.js";
|
|
4
|
+
export interface CleanupSummaryRoot {
|
|
5
|
+
versionId: number;
|
|
6
|
+
digest: string;
|
|
7
|
+
manifestKind?: ManifestKind;
|
|
8
|
+
rootTags: string[];
|
|
9
|
+
matchedTags: string[];
|
|
10
|
+
selectionMode: DeletePlanSelectionMode;
|
|
11
|
+
selectionReason: DeletePlanSelectionReason;
|
|
12
|
+
validationStatus: DeletePlanValidationStatus;
|
|
13
|
+
validationReasonCode: DeletePlanValidationReasonCode;
|
|
14
|
+
validationReason: string;
|
|
15
|
+
blockingVersionId?: number;
|
|
16
|
+
blockingDigest?: string;
|
|
17
|
+
overlapDigest?: string;
|
|
18
|
+
overlapManifestKind?: ManifestKind;
|
|
19
|
+
}
|
|
20
|
+
export interface CleanupSummaryAffectedManifest {
|
|
21
|
+
digest: string;
|
|
22
|
+
manifestKind?: ManifestKind;
|
|
23
|
+
}
|
|
24
|
+
export interface CleanupSummaryChanges {
|
|
25
|
+
deletedTags: number;
|
|
26
|
+
deletedImages: number;
|
|
27
|
+
deletedIndexes: number;
|
|
28
|
+
deletedMultiArchManifests: number;
|
|
29
|
+
deletedArtifactManifests: number;
|
|
30
|
+
deletedAttestations: number;
|
|
31
|
+
deletedSignatures: number;
|
|
32
|
+
deletedTotal: number;
|
|
33
|
+
}
|
|
34
|
+
export interface CleanupSummary {
|
|
35
|
+
command: "cleanup";
|
|
36
|
+
owner: string;
|
|
37
|
+
packageName: string;
|
|
38
|
+
scanCompletedAt: string;
|
|
39
|
+
dryRun: boolean;
|
|
40
|
+
plannerInputs: DeletePlan["plannerInputs"];
|
|
41
|
+
directTargetTags: string[];
|
|
42
|
+
collateralTags: string[];
|
|
43
|
+
fullyDeletableRoots: CleanupSummaryRoot[];
|
|
44
|
+
untagOnlyRoots: CleanupSummaryRoot[];
|
|
45
|
+
blockedRoots: CleanupSummaryRoot[];
|
|
46
|
+
affectedManifests: CleanupSummaryAffectedManifest[];
|
|
47
|
+
changes: CleanupSummaryChanges;
|
|
48
|
+
deletedPackageVersionCount: DeleteExecutionSummary["deletedPackageVersionCount"];
|
|
49
|
+
detachedTagCount: DeleteExecutionSummary["detachedTagCount"];
|
|
50
|
+
}
|
|
51
|
+
export declare function buildCleanupSummary(plan: DeletePlan, options: {
|
|
52
|
+
dryRun: boolean;
|
|
53
|
+
rootTagsByVersionId: ReadonlyMap<number, string[]>;
|
|
54
|
+
changes: CleanupSummaryChanges;
|
|
55
|
+
executionSummary?: DeleteExecutionSummary;
|
|
56
|
+
}): CleanupSummary;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DeletePlanValidationStatuses } from "../db/index.js";
|
|
2
|
+
export function buildCleanupSummary(plan, options) {
|
|
3
|
+
const directTargetTagSet = new Set(plan.directTargetTags);
|
|
4
|
+
const roots = plan.rootDecisions.map((decision) => _mapRootDecision(decision, directTargetTagSet, options.rootTagsByVersionId));
|
|
5
|
+
const fullyDeletableRoots = roots.filter((root) => root.validationStatus === DeletePlanValidationStatuses.fullyDeletable);
|
|
6
|
+
const blockedRoots = roots.filter((root) => root.validationStatus === DeletePlanValidationStatuses.blocked);
|
|
7
|
+
const untagOnlyRoots = roots.filter((root) => root.validationStatus === DeletePlanValidationStatuses.untagOnly);
|
|
8
|
+
const affectedManifests = _listAffectedManifests(plan, fullyDeletableRoots.map((root) => root.digest));
|
|
9
|
+
return {
|
|
10
|
+
command: "cleanup",
|
|
11
|
+
owner: plan.owner,
|
|
12
|
+
packageName: plan.packageName,
|
|
13
|
+
scanCompletedAt: plan.scanCompletedAt,
|
|
14
|
+
dryRun: options.dryRun,
|
|
15
|
+
plannerInputs: plan.plannerInputs,
|
|
16
|
+
directTargetTags: plan.directTargetTags,
|
|
17
|
+
collateralTags: plan.collateralTags,
|
|
18
|
+
fullyDeletableRoots,
|
|
19
|
+
untagOnlyRoots,
|
|
20
|
+
blockedRoots,
|
|
21
|
+
affectedManifests,
|
|
22
|
+
changes: options.changes,
|
|
23
|
+
deletedPackageVersionCount: options.executionSummary?.deletedPackageVersionCount ?? 0,
|
|
24
|
+
detachedTagCount: options.executionSummary?.detachedTagCount ?? 0
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function _mapRootDecision(decision, directTargetTagSet, rootTagsByVersionId) {
|
|
28
|
+
const rootTags = rootTagsByVersionId.get(decision.versionId) ?? [];
|
|
29
|
+
return {
|
|
30
|
+
versionId: decision.versionId,
|
|
31
|
+
digest: decision.digest,
|
|
32
|
+
manifestKind: decision.manifestKind,
|
|
33
|
+
rootTags,
|
|
34
|
+
matchedTags: rootTags.filter((tag) => directTargetTagSet.has(tag)),
|
|
35
|
+
selectionMode: decision.selectionMode,
|
|
36
|
+
selectionReason: decision.selectionReason,
|
|
37
|
+
validationStatus: decision.validationStatus,
|
|
38
|
+
validationReasonCode: decision.validationReasonCode,
|
|
39
|
+
validationReason: decision.validationReason,
|
|
40
|
+
blockingVersionId: decision.blockingVersionId,
|
|
41
|
+
blockingDigest: decision.blockingDigest,
|
|
42
|
+
overlapDigest: decision.overlapDigest,
|
|
43
|
+
overlapManifestKind: decision.overlapManifestKind
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function _listAffectedManifests(plan, fullyDeletableRootDigests) {
|
|
47
|
+
const fullyDeletableRootDigestSet = new Set(fullyDeletableRootDigests);
|
|
48
|
+
const manifestsByDigest = new Map();
|
|
49
|
+
for (const manifest of plan.closureManifests) {
|
|
50
|
+
if (!fullyDeletableRootDigestSet.has(manifest.sourceDigest)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
manifestsByDigest.set(manifest.memberDigest, {
|
|
54
|
+
digest: manifest.memberDigest,
|
|
55
|
+
manifestKind: manifest.memberManifestKind
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return [...manifestsByDigest.values()].sort((left, right) => left.digest.localeCompare(right.digest));
|
|
59
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type LogLevel } from "./_logger.js";
|
|
2
|
+
export declare function requireOption(args: string[], name: string): string;
|
|
3
|
+
export declare function findOption(args: string[], name: string): string | undefined;
|
|
4
|
+
export declare function collectRepeatedOption(args: string[], name: string): string[];
|
|
5
|
+
export declare function hasFlag(args: string[], name: string): boolean;
|
|
6
|
+
export declare function resolveToken(args: string[]): string;
|
|
7
|
+
export declare function resolveLogLevel(args: string[]): LogLevel;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isLogLevel } from "./_logger.js";
|
|
2
|
+
export function requireOption(args, name) {
|
|
3
|
+
const value = findOption(args, name);
|
|
4
|
+
if (!value) {
|
|
5
|
+
throw new Error(`missing required option: ${name}`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
export function findOption(args, name) {
|
|
10
|
+
const index = args.indexOf(name);
|
|
11
|
+
if (index < 0) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return args[index + 1];
|
|
15
|
+
}
|
|
16
|
+
export function collectRepeatedOption(args, name) {
|
|
17
|
+
const values = [];
|
|
18
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
19
|
+
if (args[index] === name && args[index + 1]) {
|
|
20
|
+
values.push(args[index + 1]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return values;
|
|
24
|
+
}
|
|
25
|
+
export function hasFlag(args, name) {
|
|
26
|
+
return args.includes(name);
|
|
27
|
+
}
|
|
28
|
+
export function resolveToken(args) {
|
|
29
|
+
const cliToken = findOption(args, "--token");
|
|
30
|
+
if (cliToken) {
|
|
31
|
+
return cliToken;
|
|
32
|
+
}
|
|
33
|
+
throw new Error("missing required option: --token");
|
|
34
|
+
}
|
|
35
|
+
export function resolveLogLevel(args) {
|
|
36
|
+
const rawLevel = findOption(args, "--log-level") ?? "info";
|
|
37
|
+
if (!isLogLevel(rawLevel)) {
|
|
38
|
+
throw new Error(`invalid log level: ${rawLevel}`);
|
|
39
|
+
}
|
|
40
|
+
return rawLevel;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleCleanup(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { ManifestKinds } from "../core/index.js";
|
|
2
|
+
import { buildCleanupSummary } from "../cleanup-summary/index.js";
|
|
3
|
+
import { CleanupRunWriter, openDatabase, PlannerRepository } from "../db/index.js";
|
|
4
|
+
import { executeDeletePlan } from "../execute/index.js";
|
|
5
|
+
import { hasFlag, resolveLogLevel, resolveToken } from "./_args.js";
|
|
6
|
+
import { writeJsonOutput } from "./_json-output.js";
|
|
7
|
+
import { createLogger } from "./_logger.js";
|
|
8
|
+
import { loadDeletePlan, resolvePlanCommandInputs } from "./_planner-options.js";
|
|
9
|
+
import { resolveTagSelectors } from "./_tag-selector-resolver.js";
|
|
10
|
+
export async function handleCleanup(args) {
|
|
11
|
+
const inputs = resolvePlanCommandInputs(args);
|
|
12
|
+
const dryRun = hasFlag(args, "--dry-run");
|
|
13
|
+
const token = dryRun ? undefined : resolveToken(args);
|
|
14
|
+
const logger = createLogger(resolveLogLevel(args));
|
|
15
|
+
const database = openDatabase(inputs.databasePath);
|
|
16
|
+
try {
|
|
17
|
+
const repository = new PlannerRepository(database, logger);
|
|
18
|
+
const cleanupRunWriter = new CleanupRunWriter(database);
|
|
19
|
+
const scanId = repository.getLatestCompletedScanId(inputs.owner, inputs.packageName);
|
|
20
|
+
logger.debug(`Starting cleanup for ${inputs.owner}/${inputs.packageName}`);
|
|
21
|
+
const plan = loadDeletePlan(repository, resolveTagSelectors(database, inputs));
|
|
22
|
+
const rootTagsByVersionId = _loadRootTagsByVersionId(database, inputs.owner, inputs.packageName, plan.rootDecisions.map((decision) => decision.versionId));
|
|
23
|
+
const cleanupRunId = cleanupRunWriter.persistCleanupRun(scanId, plan, {
|
|
24
|
+
dryRun,
|
|
25
|
+
cleanupStartedAt: new Date().toISOString()
|
|
26
|
+
});
|
|
27
|
+
if (dryRun) {
|
|
28
|
+
const summary = buildCleanupSummary(plan, {
|
|
29
|
+
dryRun: true,
|
|
30
|
+
rootTagsByVersionId,
|
|
31
|
+
changes: _loadSummaryChanges(database, cleanupRunId)
|
|
32
|
+
});
|
|
33
|
+
logger.debug(`Completed dry-run cleanup for ${inputs.owner}/${inputs.packageName}`);
|
|
34
|
+
writeJsonOutput(args, "--summary-json-path", summary);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
const executionSummary = await executeDeletePlan(plan, {
|
|
38
|
+
token: token,
|
|
39
|
+
logger,
|
|
40
|
+
listRootTags: (root) => _listRootTags(database, root.owner, root.packageName, root.versionId)
|
|
41
|
+
});
|
|
42
|
+
const summary = buildCleanupSummary(plan, {
|
|
43
|
+
dryRun: false,
|
|
44
|
+
rootTagsByVersionId,
|
|
45
|
+
changes: _loadSummaryChanges(database, cleanupRunId),
|
|
46
|
+
executionSummary
|
|
47
|
+
});
|
|
48
|
+
logger.debug(`Completed cleanup for ${inputs.owner}/${inputs.packageName}`);
|
|
49
|
+
writeJsonOutput(args, "--summary-json-path", summary);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
database.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function _listRootTags(database, owner, packageName, versionId) {
|
|
57
|
+
const rows = database
|
|
58
|
+
.prepare(`
|
|
59
|
+
SELECT tags.tag
|
|
60
|
+
FROM tags
|
|
61
|
+
INNER JOIN v_latest_scan_per_package latest_scan ON latest_scan.scan_id = tags.scan_id
|
|
62
|
+
WHERE latest_scan.owner = ?
|
|
63
|
+
AND latest_scan.package_name = ?
|
|
64
|
+
AND tags.version_id = ?
|
|
65
|
+
AND tags.is_digest_tag = 0
|
|
66
|
+
ORDER BY tags.tag
|
|
67
|
+
`)
|
|
68
|
+
.all(owner, packageName, versionId);
|
|
69
|
+
return rows.map((row) => row.tag);
|
|
70
|
+
}
|
|
71
|
+
function _loadRootTagsByVersionId(database, owner, packageName, versionIds) {
|
|
72
|
+
const requestedVersionIds = new Set(versionIds);
|
|
73
|
+
const tagsByVersionId = new Map();
|
|
74
|
+
for (const versionId of requestedVersionIds) {
|
|
75
|
+
tagsByVersionId.set(versionId, []);
|
|
76
|
+
}
|
|
77
|
+
if (requestedVersionIds.size === 0) {
|
|
78
|
+
return tagsByVersionId;
|
|
79
|
+
}
|
|
80
|
+
const rows = database
|
|
81
|
+
.prepare(`
|
|
82
|
+
SELECT tags.version_id, tags.tag
|
|
83
|
+
FROM tags
|
|
84
|
+
INNER JOIN v_latest_scan_per_package latest_scan ON latest_scan.scan_id = tags.scan_id
|
|
85
|
+
WHERE latest_scan.owner = ?
|
|
86
|
+
AND latest_scan.package_name = ?
|
|
87
|
+
AND tags.is_digest_tag = 0
|
|
88
|
+
ORDER BY tags.version_id, tags.tag
|
|
89
|
+
`)
|
|
90
|
+
.all(owner, packageName);
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
if (!requestedVersionIds.has(row.version_id)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
tagsByVersionId.get(row.version_id)?.push(row.tag);
|
|
96
|
+
}
|
|
97
|
+
return tagsByVersionId;
|
|
98
|
+
}
|
|
99
|
+
function _loadSummaryChanges(database, cleanupRunId) {
|
|
100
|
+
const deletedTags = database
|
|
101
|
+
.prepare(`
|
|
102
|
+
SELECT COUNT(*) AS count
|
|
103
|
+
FROM cleanup_selected_tags
|
|
104
|
+
WHERE cleanup_run_id = ?
|
|
105
|
+
AND is_deleted = 1
|
|
106
|
+
`)
|
|
107
|
+
.get(cleanupRunId).count;
|
|
108
|
+
const manifestCounts = database
|
|
109
|
+
.prepare(`
|
|
110
|
+
WITH fully_deletable_manifests AS (
|
|
111
|
+
SELECT DISTINCT
|
|
112
|
+
reachable.descendant_digest AS digest,
|
|
113
|
+
manifest.manifest_kind
|
|
114
|
+
FROM cleanup_root_decisions decision
|
|
115
|
+
JOIN manifest_reachability reachable
|
|
116
|
+
ON reachable.scan_id = decision.scan_id
|
|
117
|
+
AND reachable.ancestor_digest = decision.digest
|
|
118
|
+
JOIN manifests manifest
|
|
119
|
+
ON manifest.scan_id = reachable.scan_id
|
|
120
|
+
AND manifest.digest = reachable.descendant_digest
|
|
121
|
+
WHERE decision.cleanup_run_id = ?
|
|
122
|
+
AND decision.validation_status = 'fully-deletable'
|
|
123
|
+
)
|
|
124
|
+
SELECT
|
|
125
|
+
manifest_kind,
|
|
126
|
+
COUNT(*) AS count
|
|
127
|
+
FROM fully_deletable_manifests
|
|
128
|
+
GROUP BY manifest_kind
|
|
129
|
+
`)
|
|
130
|
+
.all(cleanupRunId);
|
|
131
|
+
const countsByKind = new Map(manifestCounts.map((row) => [row.manifest_kind ?? "", row.count]));
|
|
132
|
+
return {
|
|
133
|
+
deletedTags,
|
|
134
|
+
deletedImages: countsByKind.get(ManifestKinds.imageManifest) ?? 0,
|
|
135
|
+
deletedIndexes: countsByKind.get(ManifestKinds.indexManifest) ?? 0,
|
|
136
|
+
deletedMultiArchManifests: countsByKind.get(ManifestKinds.multiArchManifest) ?? 0,
|
|
137
|
+
deletedArtifactManifests: countsByKind.get(ManifestKinds.artifactManifest) ?? 0,
|
|
138
|
+
deletedAttestations: countsByKind.get(ManifestKinds.attestationManifest) ?? 0,
|
|
139
|
+
deletedSignatures: countsByKind.get(ManifestKinds.signatureManifest) ?? 0,
|
|
140
|
+
deletedTotal: manifestCounts.reduce((total, row) => total + row.count, 0)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleDbMerge(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { collectRepeatedOption, requireOption } from "./_args.js";
|
|
2
|
+
import { DbMergeRepository, openDatabase } from "../db/index.js";
|
|
3
|
+
export async function handleDbMerge(args) {
|
|
4
|
+
const databasePath = requireOption(args, "--db");
|
|
5
|
+
const sourceDatabasePaths = collectRepeatedOption(args, "--source-db");
|
|
6
|
+
if (sourceDatabasePaths.length === 0) {
|
|
7
|
+
throw new Error("missing required option: --source-db");
|
|
8
|
+
}
|
|
9
|
+
const targetDatabase = openDatabase(databasePath);
|
|
10
|
+
try {
|
|
11
|
+
const merger = new DbMergeRepository(targetDatabase);
|
|
12
|
+
const summaries = [];
|
|
13
|
+
let importedScanCount = 0;
|
|
14
|
+
let skippedScanCount = 0;
|
|
15
|
+
let importedCleanupRunCount = 0;
|
|
16
|
+
let skippedCleanupRunCount = 0;
|
|
17
|
+
for (const sourceDatabasePath of sourceDatabasePaths) {
|
|
18
|
+
const sourceDatabase = openDatabase(sourceDatabasePath);
|
|
19
|
+
sourceDatabase.close();
|
|
20
|
+
const summary = merger.mergeSourceDatabase(sourceDatabasePath);
|
|
21
|
+
summaries.push(summary);
|
|
22
|
+
importedScanCount += summary.importedScanCount;
|
|
23
|
+
skippedScanCount += summary.skippedScanCount;
|
|
24
|
+
importedCleanupRunCount += summary.importedCleanupRunCount;
|
|
25
|
+
skippedCleanupRunCount += summary.skippedCleanupRunCount;
|
|
26
|
+
}
|
|
27
|
+
console.log(JSON.stringify({
|
|
28
|
+
targetDatabasePath: databasePath,
|
|
29
|
+
sourceDatabaseCount: sourceDatabasePaths.length,
|
|
30
|
+
importedScanCount,
|
|
31
|
+
skippedScanCount,
|
|
32
|
+
importedCleanupRunCount,
|
|
33
|
+
skippedCleanupRunCount,
|
|
34
|
+
sources: summaries
|
|
35
|
+
}, null, 2));
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
targetDatabase.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface GitHubScanOutputs {
|
|
2
|
+
owner: string;
|
|
3
|
+
packageName: string;
|
|
4
|
+
scanCompletedAt: string;
|
|
5
|
+
packageVersions: number;
|
|
6
|
+
tags: number;
|
|
7
|
+
manifests: number;
|
|
8
|
+
manifestEdges: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function writeGitHubScanOutputs(outputPath: string, outputs: GitHubScanOutputs): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
export function writeGitHubScanOutputs(outputPath, outputs) {
|
|
3
|
+
const lines = [
|
|
4
|
+
`owner=${outputs.owner}`,
|
|
5
|
+
`package_name=${outputs.packageName}`,
|
|
6
|
+
`scan_completed_at=${outputs.scanCompletedAt}`,
|
|
7
|
+
`package_versions=${outputs.packageVersions}`,
|
|
8
|
+
`tags=${outputs.tags}`,
|
|
9
|
+
`manifests=${outputs.manifests}`,
|
|
10
|
+
`manifest_edges=${outputs.manifestEdges}`
|
|
11
|
+
];
|
|
12
|
+
appendFileSync(outputPath, `${lines.join("\n")}\n`);
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeJsonOutput(args: string[], optionName: string, payload: unknown): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { findOption } from "./_args.js";
|
|
3
|
+
export function writeJsonOutput(args, optionName, payload) {
|
|
4
|
+
const json = JSON.stringify(payload);
|
|
5
|
+
const outputPath = findOption(args, optionName);
|
|
6
|
+
if (outputPath) {
|
|
7
|
+
writeFileSync(outputPath, `${json}\n`, "utf8");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
console.log(json);
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "silent";
|
|
2
|
+
export interface Logger {
|
|
3
|
+
trace(message: string): void;
|
|
4
|
+
debug(message: string): void;
|
|
5
|
+
info(message: string): void;
|
|
6
|
+
warn(message: string): void;
|
|
7
|
+
error(message: string): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function isLogLevel(value: string): value is LogLevel;
|
|
10
|
+
export declare function createLogger(level: LogLevel, sink?: NodeJS.WritableStream): Logger;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const _logLevelPriority = {
|
|
2
|
+
trace: 0,
|
|
3
|
+
debug: 10,
|
|
4
|
+
info: 20,
|
|
5
|
+
warn: 30,
|
|
6
|
+
error: 40,
|
|
7
|
+
silent: 50
|
|
8
|
+
};
|
|
9
|
+
export function isLogLevel(value) {
|
|
10
|
+
return value in _logLevelPriority;
|
|
11
|
+
}
|
|
12
|
+
export function createLogger(level, sink = process.stderr) {
|
|
13
|
+
return {
|
|
14
|
+
trace: _write.bind(null, "trace", level, sink),
|
|
15
|
+
debug: _write.bind(null, "debug", level, sink),
|
|
16
|
+
info: _write.bind(null, "info", level, sink),
|
|
17
|
+
warn: _write.bind(null, "warn", level, sink),
|
|
18
|
+
error: _write.bind(null, "error", level, sink)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function _write(level, threshold, sink, message) {
|
|
22
|
+
if (_logLevelPriority[level] < _logLevelPriority[threshold]) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
sink.write(`${new Date().toISOString()} ${level.toUpperCase()} ${message}\n`);
|
|
26
|
+
}
|