ghcr-manager 0.0.6 → 0.9.1
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 +44 -1
- package/README.md +166 -57
- package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +6 -0
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +113 -0
- package/dist/cleanup-summary/_cleanup-summary.d.ts +40 -0
- package/dist/cleanup-summary/_cleanup-summary.js +40 -0
- package/dist/cleanup-summary/index.d.ts +2 -0
- package/dist/cleanup-summary/index.js +2 -0
- package/dist/cli/_args.d.ts +1 -0
- package/dist/cli/_args.js +3 -0
- package/dist/cli/_cleanup-command.d.ts +1 -0
- package/dist/cli/_cleanup-command.js +63 -0
- package/dist/cli/_db-merge-command.d.ts +1 -0
- package/dist/cli/_db-merge-command.js +41 -0
- package/dist/cli/_github-output.d.ts +10 -0
- package/dist/cli/_github-output.js +13 -0
- package/dist/cli/_logger.d.ts +2 -1
- package/dist/cli/_logger.js +2 -0
- package/dist/cli/_older-than.d.ts +5 -0
- package/dist/cli/_older-than.js +42 -0
- package/dist/cli/_planner-options.d.ts +20 -0
- package/dist/cli/_planner-options.js +101 -0
- package/dist/cli/_scan-command.js +11 -4
- package/dist/cli/_tag-selector-resolver.d.ts +3 -0
- package/dist/cli/_tag-selector-resolver.js +109 -0
- package/dist/cli/_untag-command.d.ts +1 -0
- package/dist/cli/_untag-command.js +57 -0
- package/dist/cli/index.js +19 -1
- package/dist/config/_service-constants.d.ts +3 -0
- package/dist/config/_service-constants.js +3 -0
- package/dist/{tuning → config}/index.d.ts +3 -0
- package/dist/{tuning → config}/index.js +3 -0
- package/dist/core/_github-package-owner.d.ts +11 -0
- package/dist/core/_github-package-owner.js +45 -0
- package/dist/core/_http-error.d.ts +6 -0
- package/dist/core/_http-error.js +33 -0
- package/dist/core/_types.d.ts +3 -2
- package/dist/core/index.d.ts +4 -1
- package/dist/core/index.js +2 -1
- package/dist/db/_cleanup-run-writer.d.ts +10 -0
- package/dist/db/_cleanup-run-writer.js +73 -0
- package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
- package/dist/db/_db-merge-cleanup-copy.js +122 -0
- package/dist/db/_db-merge-history.d.ts +2 -0
- package/dist/db/_db-merge-history.js +15 -0
- package/dist/db/_db-merge-repository.d.ts +8 -0
- package/dist/db/_db-merge-repository.js +95 -0
- package/dist/db/_db-merge-scan-copy.d.ts +10 -0
- package/dist/db/_db-merge-scan-copy.js +69 -0
- package/dist/db/_db-merge-types.d.ts +44 -0
- package/dist/db/_db-merge-types.js +1 -0
- package/dist/db/_github-actions-run-url.d.ts +1 -0
- package/dist/db/_github-actions-run-url.js +9 -0
- package/dist/db/_scan-writer.d.ts +3 -1
- package/dist/db/_scan-writer.js +28 -13
- package/dist/db/_snapshot-repository.d.ts +9 -9
- package/dist/db/_snapshot-repository.js +37 -49
- package/dist/db/_sql-placeholders.d.ts +2 -0
- package/dist/db/_sql-placeholders.js +16 -0
- package/dist/db/index.d.ts +5 -0
- package/dist/db/index.js +3 -0
- package/dist/db/planner/_planner-delete-tag-root-targets.d.ts +7 -0
- package/dist/db/planner/_planner-delete-tag-root-targets.js +130 -0
- package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
- package/dist/db/planner/_planner-direct-target-tags.js +47 -0
- package/dist/db/planner/_planner-keep-tagged-root-targets.d.ts +7 -0
- package/dist/db/planner/_planner-keep-tagged-root-targets.js +74 -0
- package/dist/db/planner/_planner-output.d.ts +5 -0
- package/dist/db/planner/_planner-output.js +101 -0
- package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
- package/dist/db/planner/_planner-plan-artifacts.js +211 -0
- package/dist/db/planner/_planner-repository.d.ts +34 -0
- package/dist/db/planner/_planner-repository.js +126 -0
- package/dist/db/planner/_planner-sql.d.ts +12 -0
- package/dist/db/planner/_planner-sql.js +35 -0
- package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
- package/dist/db/planner/_planner-tag-selectors.js +57 -0
- package/dist/db/planner/_planner-tagged-root-targets.d.ts +15 -0
- package/dist/db/planner/_planner-tagged-root-targets.js +19 -0
- package/dist/db/planner/_planner-tagged-targets.d.ts +8 -0
- package/dist/db/planner/_planner-tagged-targets.js +16 -0
- package/dist/db/planner/_planner-types.d.ts +135 -0
- package/dist/db/planner/_planner-types.js +38 -0
- package/dist/db/planner/_planner-untagged-targets.d.ts +9 -0
- package/dist/db/planner/_planner-untagged-targets.js +91 -0
- package/dist/db/planner/index.d.ts +2 -0
- package/dist/db/planner/index.js +1 -0
- package/dist/execute/_http.d.ts +7 -0
- package/dist/execute/_http.js +48 -0
- package/dist/execute/_manifest-detach.d.ts +4 -0
- package/dist/execute/_manifest-detach.js +31 -0
- package/dist/execute/_package-version-delete-client.d.ts +4 -0
- package/dist/execute/_package-version-delete-client.js +34 -0
- package/dist/execute/_package-version-page-client.d.ts +14 -0
- package/dist/execute/_package-version-page-client.js +64 -0
- package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
- package/dist/execute/_package-version-tag-source-client.js +65 -0
- package/dist/execute/_plan-executor.d.ts +3 -0
- package/dist/execute/_plan-executor.js +47 -0
- package/dist/execute/_registry-manifest-client.d.ts +12 -0
- package/dist/execute/_registry-manifest-client.js +79 -0
- package/dist/execute/_registry-token-client.d.ts +4 -0
- package/dist/execute/_registry-token-client.js +37 -0
- package/dist/execute/_types.d.ts +51 -0
- package/dist/execute/_types.js +1 -0
- package/dist/execute/_untag-client.d.ts +2 -0
- package/dist/execute/_untag-client.js +71 -0
- package/dist/execute/index.d.ts +5 -0
- package/dist/execute/index.js +3 -0
- package/dist/ingest/github/_manifest-client.d.ts +7 -1
- package/dist/ingest/github/_manifest-client.js +8 -0
- package/dist/ingest/github/_manifest-ingest.js +39 -53
- package/dist/ingest/github/_manifest-kind.d.ts +20 -0
- package/dist/ingest/github/_manifest-kind.js +50 -0
- package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
- package/dist/ingest/github/_package-metadata-load.js +45 -0
- package/dist/ingest/github/_package-version-page-load.d.ts +1 -1
- package/dist/ingest/github/_package-version-page-load.js +8 -5
- package/dist/ingest/github/_packages-client.d.ts +1 -1
- package/dist/ingest/github/_packages-client.js +21 -4
- package/dist/ingest/github/_parallel-paginated-ingest.d.ts +1 -0
- package/dist/ingest/github/_parallel-paginated-ingest.js +2 -2
- package/dist/ingest/github/_shared.d.ts +1 -1
- package/dist/ingest/github/_shared.js +2 -34
- package/dist/ingest/github/index.d.ts +4 -0
- package/dist/ingest/github/index.js +8 -5
- package/package.json +7 -5
- package/resources/sql/schema/001_schema.sql +82 -8
- package/resources/sql/views/001_v_latest_scan_per_package.sql +2 -2
- package/resources/sql/views/003_v_scan_root_manifests.sql +43 -0
- package/resources/sql/views/004_v_digest_derived_tag_relations.sql +51 -0
- package/resources/sql/views/005_v_cleanup_root_closure_members.sql +100 -0
- package/resources/sql/views/006_v_cleanup_blocking_overlaps.sql +42 -0
- package/dist/ingest/github/_paginated-ingest.d.ts +0 -11
- package/dist/ingest/github/_paginated-ingest.js +0 -28
- package/resources/sql/views/003_v_missing_digests_related_manifests.sql +0 -78
- package/resources/sql/views/004_v_manifests_related_manifests.sql +0 -142
|
@@ -0,0 +1,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
|
+
}
|
package/dist/cli/_logger.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
|
|
1
|
+
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "silent";
|
|
2
2
|
export interface Logger {
|
|
3
|
+
trace(message: string): void;
|
|
3
4
|
debug(message: string): void;
|
|
4
5
|
info(message: string): void;
|
|
5
6
|
warn(message: string): void;
|
package/dist/cli/_logger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const _logLevelPriority = {
|
|
2
|
+
trace: 0,
|
|
2
3
|
debug: 10,
|
|
3
4
|
info: 20,
|
|
4
5
|
warn: 30,
|
|
@@ -10,6 +11,7 @@ export function isLogLevel(value) {
|
|
|
10
11
|
}
|
|
11
12
|
export function createLogger(level, sink = process.stderr) {
|
|
12
13
|
return {
|
|
14
|
+
trace: _write.bind(null, "trace", level, sink),
|
|
13
15
|
debug: _write.bind(null, "debug", level, sink),
|
|
14
16
|
info: _write.bind(null, "info", level, sink),
|
|
15
17
|
warn: _write.bind(null, "warn", level, sink),
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function resolveOlderThan(rawValue, now) {
|
|
2
|
+
const normalized = rawValue.trim().toLowerCase();
|
|
3
|
+
const match = normalized.match(/^(\d+)\s+(minute|minutes|hour|hours|day|days|week|weeks|month|months|year|years)$/);
|
|
4
|
+
if (!match) {
|
|
5
|
+
throw new Error(`invalid older-than interval: ${rawValue}`);
|
|
6
|
+
}
|
|
7
|
+
const amount = Number(match[1]);
|
|
8
|
+
const unit = match[2];
|
|
9
|
+
const cutoff = new Date(now.getTime());
|
|
10
|
+
switch (unit) {
|
|
11
|
+
case "minute":
|
|
12
|
+
case "minutes":
|
|
13
|
+
cutoff.setUTCMinutes(cutoff.getUTCMinutes() - amount);
|
|
14
|
+
break;
|
|
15
|
+
case "hour":
|
|
16
|
+
case "hours":
|
|
17
|
+
cutoff.setUTCHours(cutoff.getUTCHours() - amount);
|
|
18
|
+
break;
|
|
19
|
+
case "day":
|
|
20
|
+
case "days":
|
|
21
|
+
cutoff.setUTCDate(cutoff.getUTCDate() - amount);
|
|
22
|
+
break;
|
|
23
|
+
case "week":
|
|
24
|
+
case "weeks":
|
|
25
|
+
cutoff.setUTCDate(cutoff.getUTCDate() - amount * 7);
|
|
26
|
+
break;
|
|
27
|
+
case "month":
|
|
28
|
+
case "months":
|
|
29
|
+
cutoff.setUTCMonth(cutoff.getUTCMonth() - amount);
|
|
30
|
+
break;
|
|
31
|
+
case "year":
|
|
32
|
+
case "years":
|
|
33
|
+
cutoff.setUTCFullYear(cutoff.getUTCFullYear() - amount);
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
throw new Error(`invalid older-than unit: ${unit}`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
olderThan: normalized,
|
|
40
|
+
cutoffTimestamp: cutoff.toISOString()
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DeletePlan, PlannerRepository } from "../db/index.js";
|
|
2
|
+
export interface PlanCommandInputs {
|
|
3
|
+
databasePath: string;
|
|
4
|
+
owner: string;
|
|
5
|
+
packageName: string;
|
|
6
|
+
deleteTags: string[];
|
|
7
|
+
deleteTagsRequested: boolean;
|
|
8
|
+
deleteGhostImages: boolean;
|
|
9
|
+
deletePartialImages: boolean;
|
|
10
|
+
deleteOrphanedImages: boolean;
|
|
11
|
+
excludeTags: string[];
|
|
12
|
+
deleteUntagged: boolean;
|
|
13
|
+
useRegex: boolean;
|
|
14
|
+
keepNTagged?: number;
|
|
15
|
+
keepNUntagged?: number;
|
|
16
|
+
olderThan?: string;
|
|
17
|
+
cutoffTimestamp?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function resolvePlanCommandInputs(args: string[]): PlanCommandInputs;
|
|
20
|
+
export declare function loadDeletePlan(repository: PlannerRepository, inputs: PlanCommandInputs): DeletePlan;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { collectRepeatedOption, hasFlag, requireOption } from "./_args.js";
|
|
2
|
+
import { resolveOlderThan } from "./_older-than.js";
|
|
3
|
+
export function resolvePlanCommandInputs(args) {
|
|
4
|
+
const databasePath = requireOption(args, "--db");
|
|
5
|
+
const owner = requireOption(args, "--owner");
|
|
6
|
+
const packageName = requireOption(args, "--package");
|
|
7
|
+
const deleteTags = collectRepeatedOption(args, "--delete-tag");
|
|
8
|
+
const deleteGhostImages = hasFlag(args, "--delete-ghost-images");
|
|
9
|
+
const deletePartialImages = hasFlag(args, "--delete-partial-images");
|
|
10
|
+
const deleteOrphanedImages = hasFlag(args, "--delete-orphaned-images");
|
|
11
|
+
const excludeTags = collectRepeatedOption(args, "--exclude-tag");
|
|
12
|
+
const deleteUntagged = hasFlag(args, "--delete-untagged");
|
|
13
|
+
const useRegex = hasFlag(args, "--use-regex");
|
|
14
|
+
const keepNTaggedRaw = collectRepeatedOption(args, "--keep-n-tagged");
|
|
15
|
+
const keepNUntaggedRaw = collectRepeatedOption(args, "--keep-n-untagged");
|
|
16
|
+
const olderThanRaw = collectRepeatedOption(args, "--older-than");
|
|
17
|
+
if (keepNTaggedRaw.length > 1) {
|
|
18
|
+
throw new Error("--keep-n-tagged may only be provided once");
|
|
19
|
+
}
|
|
20
|
+
if (keepNUntaggedRaw.length > 1) {
|
|
21
|
+
throw new Error("--keep-n-untagged may only be provided once");
|
|
22
|
+
}
|
|
23
|
+
const keepNTagged = keepNTaggedRaw[0] ? resolveKeepCount("--keep-n-tagged", keepNTaggedRaw[0]) : undefined;
|
|
24
|
+
const keepNUntagged = keepNUntaggedRaw[0] ? resolveKeepCount("--keep-n-untagged", keepNUntaggedRaw[0]) : undefined;
|
|
25
|
+
const taggedSelectorActive = deleteTags.length > 0 ||
|
|
26
|
+
deleteGhostImages ||
|
|
27
|
+
deletePartialImages ||
|
|
28
|
+
deleteOrphanedImages ||
|
|
29
|
+
keepNTagged !== undefined;
|
|
30
|
+
const selectorCount = (deleteUntagged ? 1 : 0) + (taggedSelectorActive ? 1 : 0) + (keepNUntagged !== undefined ? 1 : 0);
|
|
31
|
+
if (selectorCount > 1) {
|
|
32
|
+
throw new Error("plan currently supports exactly one selector family: --delete-untagged, --delete-tag, --delete-ghost-images, --delete-partial-images, --delete-orphaned-images, --keep-n-tagged, or --keep-n-untagged");
|
|
33
|
+
}
|
|
34
|
+
if (selectorCount === 0) {
|
|
35
|
+
throw new Error("missing required cleanup selector: --delete-untagged, --delete-tag, --delete-ghost-images, --delete-partial-images, --delete-orphaned-images, --keep-n-tagged, or --keep-n-untagged");
|
|
36
|
+
}
|
|
37
|
+
if ((deleteUntagged || keepNUntagged !== undefined) && excludeTags.length > 0) {
|
|
38
|
+
throw new Error("--exclude-tag is only supported with tagged selector families");
|
|
39
|
+
}
|
|
40
|
+
if (olderThanRaw.length > 1) {
|
|
41
|
+
throw new Error("--older-than may only be provided once");
|
|
42
|
+
}
|
|
43
|
+
const olderThan = olderThanRaw[0] ? resolveOlderThan(olderThanRaw[0], new Date()) : undefined;
|
|
44
|
+
return {
|
|
45
|
+
databasePath,
|
|
46
|
+
owner,
|
|
47
|
+
packageName,
|
|
48
|
+
deleteTags,
|
|
49
|
+
deleteTagsRequested: deleteTags.length > 0 || deleteGhostImages || deletePartialImages || deleteOrphanedImages,
|
|
50
|
+
deleteGhostImages,
|
|
51
|
+
deletePartialImages,
|
|
52
|
+
deleteOrphanedImages,
|
|
53
|
+
excludeTags,
|
|
54
|
+
deleteUntagged,
|
|
55
|
+
useRegex,
|
|
56
|
+
keepNTagged,
|
|
57
|
+
keepNUntagged,
|
|
58
|
+
olderThan: olderThan?.olderThan,
|
|
59
|
+
cutoffTimestamp: olderThan?.cutoffTimestamp
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function loadDeletePlan(repository, inputs) {
|
|
63
|
+
if (inputs.keepNUntagged !== undefined) {
|
|
64
|
+
return repository.getKeepNUntaggedPlanWithCutoff(inputs.owner, inputs.packageName, inputs.keepNUntagged, {
|
|
65
|
+
olderThan: inputs.olderThan,
|
|
66
|
+
cutoffTimestamp: inputs.cutoffTimestamp
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (inputs.deleteUntagged) {
|
|
70
|
+
return repository.getDeleteUntaggedPlanWithCutoff(inputs.owner, inputs.packageName, {
|
|
71
|
+
olderThan: inputs.olderThan,
|
|
72
|
+
cutoffTimestamp: inputs.cutoffTimestamp
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (!inputs.deleteTagsRequested &&
|
|
76
|
+
!inputs.deleteGhostImages &&
|
|
77
|
+
!inputs.deletePartialImages &&
|
|
78
|
+
!inputs.deleteOrphanedImages &&
|
|
79
|
+
inputs.keepNTagged !== undefined) {
|
|
80
|
+
return repository.getKeepNTaggedPlanWithCutoff(inputs.owner, inputs.packageName, inputs.keepNTagged, [], {
|
|
81
|
+
olderThan: inputs.olderThan,
|
|
82
|
+
cutoffTimestamp: inputs.cutoffTimestamp
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return repository.getDeleteTagsPlanWithCutoff(inputs.owner, inputs.packageName, inputs.deleteTags, inputs.excludeTags, {
|
|
86
|
+
deleteGhostImages: inputs.deleteGhostImages,
|
|
87
|
+
deletePartialImages: inputs.deletePartialImages,
|
|
88
|
+
deleteOrphanedImages: inputs.deleteOrphanedImages,
|
|
89
|
+
deleteTagsRequested: inputs.deleteTagsRequested,
|
|
90
|
+
keepNTagged: inputs.keepNTagged,
|
|
91
|
+
useRegex: inputs.useRegex,
|
|
92
|
+
olderThan: inputs.olderThan,
|
|
93
|
+
cutoffTimestamp: inputs.cutoffTimestamp
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function resolveKeepCount(optionName, rawValue) {
|
|
97
|
+
if (!/^\d+$/.test(rawValue)) {
|
|
98
|
+
throw new Error(`${optionName} must be a non-negative integer`);
|
|
99
|
+
}
|
|
100
|
+
return Number.parseInt(rawValue, 10);
|
|
101
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { ScanWriter, SnapshotRepository, openDatabase } from "../db/index.js";
|
|
2
2
|
import { importGitHubScan } from "../ingest/github/index.js";
|
|
3
|
-
import { requireOption, resolveGitHubToken, resolveLogLevel } from "./_args.js";
|
|
3
|
+
import { findOption, requireOption, resolveGitHubToken, resolveLogLevel } from "./_args.js";
|
|
4
|
+
import { writeGitHubScanOutputs } from "./_github-output.js";
|
|
4
5
|
import { createLogger } from "./_logger.js";
|
|
5
6
|
export async function handleScan(args) {
|
|
6
7
|
const databasePath = requireOption(args, "--db");
|
|
7
8
|
const owner = requireOption(args, "--owner");
|
|
8
9
|
const packageName = requireOption(args, "--package");
|
|
10
|
+
const githubOutputPath = findOption(args, "--github-output");
|
|
11
|
+
const token = resolveGitHubToken(args);
|
|
9
12
|
const logger = createLogger(resolveLogLevel(args));
|
|
10
13
|
const database = openDatabase(databasePath);
|
|
11
14
|
const repository = new SnapshotRepository(database);
|
|
@@ -13,12 +16,12 @@ export async function handleScan(args) {
|
|
|
13
16
|
await importGitHubScan({
|
|
14
17
|
owner,
|
|
15
18
|
packageName,
|
|
16
|
-
token
|
|
19
|
+
token,
|
|
17
20
|
logger
|
|
18
21
|
}, writer, repository);
|
|
19
22
|
const scanId = writer.getActiveScanId();
|
|
20
23
|
const metadata = repository.getPackageMetadata(scanId);
|
|
21
|
-
|
|
24
|
+
const summary = {
|
|
22
25
|
owner: metadata.owner,
|
|
23
26
|
packageName: metadata.packageName,
|
|
24
27
|
scanCompletedAt: metadata.scanCompletedAt,
|
|
@@ -26,7 +29,11 @@ export async function handleScan(args) {
|
|
|
26
29
|
tags: repository.countTags(scanId),
|
|
27
30
|
manifests: repository.countManifests(scanId),
|
|
28
31
|
manifestEdges: repository.countManifestEdges(scanId)
|
|
29
|
-
}
|
|
32
|
+
};
|
|
33
|
+
if (githubOutputPath) {
|
|
34
|
+
writeGitHubScanOutputs(githubOutputPath, summary);
|
|
35
|
+
}
|
|
36
|
+
console.log(JSON.stringify(summary));
|
|
30
37
|
database.close();
|
|
31
38
|
return 0;
|
|
32
39
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export function resolveTagSelectors(database, inputs) {
|
|
2
|
+
if (!inputs.deleteGhostImages && !inputs.deletePartialImages && !inputs.deleteOrphanedImages) {
|
|
3
|
+
return inputs;
|
|
4
|
+
}
|
|
5
|
+
return {
|
|
6
|
+
...inputs,
|
|
7
|
+
deleteTags: inputs.deleteGhostImages
|
|
8
|
+
? _listLatestGhostTags(database, inputs.owner, inputs.packageName, inputs.cutoffTimestamp)
|
|
9
|
+
: inputs.deletePartialImages
|
|
10
|
+
? _listLatestPartialTags(database, inputs.owner, inputs.packageName, inputs.cutoffTimestamp)
|
|
11
|
+
: inputs.deleteOrphanedImages
|
|
12
|
+
? _listLatestOrphanedTags(database, inputs.owner, inputs.packageName, inputs.cutoffTimestamp)
|
|
13
|
+
: inputs.deleteTags
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function _listLatestGhostTags(database, owner, packageName, cutoffTimestamp) {
|
|
17
|
+
return _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestamp, "all-missing");
|
|
18
|
+
}
|
|
19
|
+
function _listLatestPartialTags(database, owner, packageName, cutoffTimestamp) {
|
|
20
|
+
return _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestamp, "some-missing");
|
|
21
|
+
}
|
|
22
|
+
function _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestamp, mode) {
|
|
23
|
+
const havingClause = mode === "all-missing"
|
|
24
|
+
? "COUNT(*) > 0 AND COUNT(child.digest) = 0"
|
|
25
|
+
: "COUNT(child.digest) > 0 AND COUNT(child.digest) < COUNT(*)";
|
|
26
|
+
const rows = database
|
|
27
|
+
.prepare(`
|
|
28
|
+
WITH latest_scan AS (
|
|
29
|
+
SELECT scan_id
|
|
30
|
+
FROM v_latest_scan_per_package
|
|
31
|
+
WHERE owner = ?
|
|
32
|
+
AND package_name = ?
|
|
33
|
+
LIMIT 1
|
|
34
|
+
),
|
|
35
|
+
ghost_roots AS (
|
|
36
|
+
SELECT
|
|
37
|
+
m.scan_id,
|
|
38
|
+
m.version_id
|
|
39
|
+
FROM latest_scan ls
|
|
40
|
+
JOIN manifests m
|
|
41
|
+
ON m.scan_id = ls.scan_id
|
|
42
|
+
JOIN package_versions pv
|
|
43
|
+
ON pv.scan_id = m.scan_id
|
|
44
|
+
AND pv.version_id = m.version_id
|
|
45
|
+
JOIN tags root_tags
|
|
46
|
+
ON root_tags.scan_id = m.scan_id
|
|
47
|
+
AND root_tags.version_id = m.version_id
|
|
48
|
+
JOIN manifest_descriptors md
|
|
49
|
+
ON md.scan_id = m.scan_id
|
|
50
|
+
AND md.parent_digest = m.digest
|
|
51
|
+
LEFT JOIN manifests child
|
|
52
|
+
ON child.scan_id = md.scan_id
|
|
53
|
+
AND child.digest = md.child_digest
|
|
54
|
+
WHERE m.media_type IN (
|
|
55
|
+
'application/vnd.oci.image.index.v1+json',
|
|
56
|
+
'application/vnd.docker.distribution.manifest.list.v2+json'
|
|
57
|
+
)
|
|
58
|
+
AND NOT EXISTS (
|
|
59
|
+
SELECT 1
|
|
60
|
+
FROM manifest_reachability mr
|
|
61
|
+
WHERE mr.scan_id = m.scan_id
|
|
62
|
+
AND mr.descendant_digest = m.digest
|
|
63
|
+
AND mr.min_distance > 0
|
|
64
|
+
)
|
|
65
|
+
AND (? IS NULL OR pv.created_at < ?)
|
|
66
|
+
GROUP BY m.scan_id, m.version_id
|
|
67
|
+
HAVING ${havingClause}
|
|
68
|
+
)
|
|
69
|
+
SELECT DISTINCT t.tag
|
|
70
|
+
FROM ghost_roots gr
|
|
71
|
+
JOIN tags t
|
|
72
|
+
ON t.scan_id = gr.scan_id
|
|
73
|
+
AND t.version_id = gr.version_id
|
|
74
|
+
ORDER BY t.tag
|
|
75
|
+
`)
|
|
76
|
+
.all(owner, packageName, cutoffTimestamp ?? null, cutoffTimestamp ?? null);
|
|
77
|
+
return rows.map((row) => row.tag);
|
|
78
|
+
}
|
|
79
|
+
// Some OCI tooling publishes companion artifacts such as signatures or attestations under
|
|
80
|
+
// digest-derived tags in the same repository, for example `sha256-<digest>.sig`, while the
|
|
81
|
+
// actual relationship is the artifact's subject/referrer link to the parent digest.
|
|
82
|
+
//
|
|
83
|
+
// Public references:
|
|
84
|
+
// - Sigstore Cosign example pushing `sha256-<digest>.sig`:
|
|
85
|
+
// https://docs.sigstore.dev/cosign/signing/other_types/
|
|
86
|
+
// - OCI referrers / subject model:
|
|
87
|
+
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md
|
|
88
|
+
//
|
|
89
|
+
// This resolver intentionally mirrors the `delete-orphaned-images` behavior from
|
|
90
|
+
// `dataaxiom/ghcr-cleanup-action`, but keeps the check narrow and local to the current package
|
|
91
|
+
// scan: derive the parent digest from the tag, then treat the tag as orphaned only when that
|
|
92
|
+
// digest is absent from the scanned manifests for the same package.
|
|
93
|
+
function _listLatestOrphanedTags(database, owner, packageName, cutoffTimestamp) {
|
|
94
|
+
const rows = database
|
|
95
|
+
.prepare(`
|
|
96
|
+
SELECT DISTINCT dtr.tag
|
|
97
|
+
FROM v_digest_derived_tag_relations dtr
|
|
98
|
+
INNER JOIN package_versions pv
|
|
99
|
+
ON pv.scan_id = dtr.scan_id
|
|
100
|
+
AND pv.version_id = dtr.artifact_version_id
|
|
101
|
+
WHERE dtr.owner = ?
|
|
102
|
+
AND dtr.package_name = ?
|
|
103
|
+
AND dtr.parent_exists = 0
|
|
104
|
+
AND (? IS NULL OR pv.created_at < ?)
|
|
105
|
+
ORDER BY dtr.tag
|
|
106
|
+
`)
|
|
107
|
+
.all(owner, packageName, cutoffTimestamp ?? null, cutoffTimestamp ?? null);
|
|
108
|
+
return rows.map((row) => row.tag);
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleUntag(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { listPackageVersionTagSources, untagRootTags } from "../execute/index.js";
|
|
2
|
+
import { collectRepeatedOption, hasFlag, requireOption, resolveGitHubToken, resolveLogLevel } from "./_args.js";
|
|
3
|
+
import { createLogger } from "./_logger.js";
|
|
4
|
+
export async function handleUntag(args) {
|
|
5
|
+
const owner = requireOption(args, "--owner");
|
|
6
|
+
const packageName = requireOption(args, "--package");
|
|
7
|
+
const requestedTags = [...new Set(collectRepeatedOption(args, "--tag"))];
|
|
8
|
+
if (requestedTags.length === 0) {
|
|
9
|
+
throw new Error("missing required option: --tag");
|
|
10
|
+
}
|
|
11
|
+
const token = resolveGitHubToken(args);
|
|
12
|
+
const dryRun = hasFlag(args, "--dry-run");
|
|
13
|
+
const logger = createLogger(resolveLogLevel(args));
|
|
14
|
+
const tagSources = await listPackageVersionTagSources(owner, packageName, requestedTags, token, logger);
|
|
15
|
+
const matchedTags = new Set(tagSources.map((tagSource) => tagSource.tag));
|
|
16
|
+
const missingTags = requestedTags.filter((tag) => !matchedTags.has(tag));
|
|
17
|
+
if (missingTags.length > 0) {
|
|
18
|
+
throw new Error(`could not resolve tag(s): ${missingTags.join(", ")}`);
|
|
19
|
+
}
|
|
20
|
+
const roots = _groupTagSources(tagSources);
|
|
21
|
+
const untaggedTags = [];
|
|
22
|
+
if (!dryRun) {
|
|
23
|
+
for (const root of roots) {
|
|
24
|
+
untaggedTags.push(...(await untagRootTags(owner, packageName, root.versionId, root.digest, root.tags, {
|
|
25
|
+
token,
|
|
26
|
+
logger
|
|
27
|
+
})));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const summary = {
|
|
31
|
+
owner,
|
|
32
|
+
packageName,
|
|
33
|
+
requestedTags,
|
|
34
|
+
dryRun,
|
|
35
|
+
roots,
|
|
36
|
+
untaggedTags
|
|
37
|
+
};
|
|
38
|
+
console.log(JSON.stringify(summary));
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
function _groupTagSources(tagSources) {
|
|
42
|
+
const groups = new Map();
|
|
43
|
+
for (const tagSource of tagSources) {
|
|
44
|
+
const key = `${tagSource.sourceVersionId}:${tagSource.sourceDigest}`;
|
|
45
|
+
const existing = groups.get(key);
|
|
46
|
+
if (existing) {
|
|
47
|
+
existing.tags.push(tagSource.tag);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
groups.set(key, {
|
|
51
|
+
versionId: tagSource.sourceVersionId,
|
|
52
|
+
digest: tagSource.sourceDigest,
|
|
53
|
+
tags: [tagSource.tag]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return [...groups.values()];
|
|
57
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { handleCleanup } from "./_cleanup-command.js";
|
|
5
|
+
import { handleDbMerge } from "./_db-merge-command.js";
|
|
4
6
|
import { handleScan } from "./_scan-command.js";
|
|
7
|
+
import { handleUntag } from "./_untag-command.js";
|
|
5
8
|
export async function main(argv) {
|
|
6
9
|
const [command, ...rest] = argv;
|
|
7
10
|
if (!command) {
|
|
@@ -9,15 +12,30 @@ export async function main(argv) {
|
|
|
9
12
|
return 1;
|
|
10
13
|
}
|
|
11
14
|
switch (command) {
|
|
15
|
+
case "cleanup":
|
|
16
|
+
return handleCleanup(rest);
|
|
17
|
+
case "db-merge":
|
|
18
|
+
return handleDbMerge(rest);
|
|
12
19
|
case "scan":
|
|
13
20
|
return handleScan(rest);
|
|
21
|
+
case "untag":
|
|
22
|
+
return handleUntag(rest);
|
|
14
23
|
default:
|
|
15
24
|
throw new Error(`unknown command: ${command}`);
|
|
16
25
|
}
|
|
17
26
|
}
|
|
18
27
|
function printUsage() {
|
|
19
28
|
console.error(`Usage:
|
|
20
|
-
ghcr-manager
|
|
29
|
+
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --delete-untagged [--older-than <interval>]
|
|
30
|
+
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --delete-ghost-images [--exclude-tag <tag> ...] [--use-regex] [--keep-n-tagged <count>] [--older-than <interval>]
|
|
31
|
+
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --delete-partial-images [--exclude-tag <tag> ...] [--use-regex] [--keep-n-tagged <count>] [--older-than <interval>]
|
|
32
|
+
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --delete-orphaned-images [--exclude-tag <tag> ...] [--use-regex] [--keep-n-tagged <count>] [--older-than <interval>]
|
|
33
|
+
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --keep-n-tagged <count> [--older-than <interval>]
|
|
34
|
+
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --keep-n-untagged <count> [--older-than <interval>]
|
|
35
|
+
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --delete-tag <tag> [--delete-tag <tag> ...] [--exclude-tag <tag> ...] [--use-regex] [--older-than <interval>]
|
|
36
|
+
ghcr-manager db-merge --db <target-path> --source-db <path> [--source-db <path> ...]
|
|
37
|
+
ghcr-manager scan --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--github-output <path>] --owner <org> --package <name> --token <token>
|
|
38
|
+
ghcr-manager untag [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> --token <token> --tag <tag> [--tag <tag> ...]`);
|
|
21
39
|
}
|
|
22
40
|
const _entryPath = process.argv[1];
|
|
23
41
|
const _isDirectExecution = realpathSync(_entryPath) === realpathSync(fileURLToPath(import.meta.url));
|
|
@@ -2,5 +2,8 @@ export declare const packageVersionPageFetchConcurrency = 4;
|
|
|
2
2
|
export declare const manifestFetchConcurrency = 16;
|
|
3
3
|
export declare const ingestRequestRetryCount = 3;
|
|
4
4
|
export declare const ingestRequestRetryDelayMs = 1000;
|
|
5
|
+
export declare const executeRequestRetryCount = 3;
|
|
6
|
+
export declare const executeRequestRetryDelayMs = 1000;
|
|
5
7
|
export declare const paginatedIngestProgressIntervalPages = 10;
|
|
6
8
|
export declare const manifestIngestProgressStepRatio = 0.05;
|
|
9
|
+
export { ghcrRegistryBaseUrl, githubApiBaseUrl, githubApiVersion } from "./_service-constants.js";
|
|
@@ -2,5 +2,8 @@ export const packageVersionPageFetchConcurrency = 4;
|
|
|
2
2
|
export const manifestFetchConcurrency = 16;
|
|
3
3
|
export const ingestRequestRetryCount = 3;
|
|
4
4
|
export const ingestRequestRetryDelayMs = 1000;
|
|
5
|
+
export const executeRequestRetryCount = 3;
|
|
6
|
+
export const executeRequestRetryDelayMs = 1000;
|
|
5
7
|
export const paginatedIngestProgressIntervalPages = 10;
|
|
6
8
|
export const manifestIngestProgressStepRatio = 0.05;
|
|
9
|
+
export { ghcrRegistryBaseUrl, githubApiBaseUrl, githubApiVersion } from "./_service-constants.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface _FetchLikeResponse {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
status: number;
|
|
4
|
+
headers: Headers;
|
|
5
|
+
json(): Promise<unknown>;
|
|
6
|
+
}
|
|
7
|
+
interface _Logger {
|
|
8
|
+
warn(message: string): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function getOwnerURIComponent(fetchImpl: (input: string, init?: RequestInit) => Promise<_FetchLikeResponse>, owner: string, token: string, logger: _Logger): Promise<string>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { githubApiBaseUrl, githubApiVersion, ingestRequestRetryCount, ingestRequestRetryDelayMs } from "../config/index.js";
|
|
2
|
+
import { buildHttpErrorMessage } from "./_http-error.js";
|
|
3
|
+
const _ownerUriComponentByOwner = new Map();
|
|
4
|
+
export async function getOwnerURIComponent(fetchImpl, owner, token, logger) {
|
|
5
|
+
const cachedOwnerURIComponent = _ownerUriComponentByOwner.get(owner);
|
|
6
|
+
if (cachedOwnerURIComponent) {
|
|
7
|
+
return cachedOwnerURIComponent;
|
|
8
|
+
}
|
|
9
|
+
const url = new URL(`/users/${encodeURIComponent(owner)}`, githubApiBaseUrl).toString();
|
|
10
|
+
for (let attempt = 1;; attempt += 1) {
|
|
11
|
+
const response = await fetchImpl(url, {
|
|
12
|
+
headers: {
|
|
13
|
+
Accept: "application/vnd.github+json",
|
|
14
|
+
Authorization: `Bearer ${token}`,
|
|
15
|
+
"User-Agent": "ghcr-manager",
|
|
16
|
+
"X-GitHub-Api-Version": githubApiVersion
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
if (response.ok) {
|
|
20
|
+
const payload = (await response.json());
|
|
21
|
+
if (payload.type === "Organization") {
|
|
22
|
+
const ownerURIComponent = `orgs/${encodeURIComponent(owner)}`;
|
|
23
|
+
_ownerUriComponentByOwner.set(owner, ownerURIComponent);
|
|
24
|
+
return ownerURIComponent;
|
|
25
|
+
}
|
|
26
|
+
if (payload.type === "User") {
|
|
27
|
+
const ownerURIComponent = `users/${encodeURIComponent(owner)}`;
|
|
28
|
+
_ownerUriComponentByOwner.set(owner, ownerURIComponent);
|
|
29
|
+
return ownerURIComponent;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`GitHub owner lookup did not include a supported type`);
|
|
32
|
+
}
|
|
33
|
+
if (!_isRetryableStatus(response.status) || attempt > ingestRequestRetryCount) {
|
|
34
|
+
throw new Error(await buildHttpErrorMessage(response, "GitHub owner lookup failed"));
|
|
35
|
+
}
|
|
36
|
+
logger.warn(`GitHub owner lookup failed on attempt ${attempt}/${ingestRequestRetryCount + 1}; retrying in ${ingestRequestRetryDelayMs}ms - ${await buildHttpErrorMessage(response, "GitHub owner lookup failed")}`);
|
|
37
|
+
await _sleep(ingestRequestRetryDelayMs);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function _isRetryableStatus(status) {
|
|
41
|
+
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
42
|
+
}
|
|
43
|
+
function _sleep(delayMs) {
|
|
44
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
45
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export async function buildHttpErrorMessage(response, fallback) {
|
|
2
|
+
const details = [fallback, `status ${response.status}`];
|
|
3
|
+
const body = await _readJsonErrorBody(response);
|
|
4
|
+
const message = typeof body?.message === "string" ? body.message : undefined;
|
|
5
|
+
const documentationUrl = typeof body?.documentation_url === "string" ? body.documentation_url : undefined;
|
|
6
|
+
const authenticateHeader = response.headers.get("www-authenticate") ?? undefined;
|
|
7
|
+
if (message) {
|
|
8
|
+
details.push(message);
|
|
9
|
+
}
|
|
10
|
+
if (documentationUrl) {
|
|
11
|
+
details.push(documentationUrl);
|
|
12
|
+
}
|
|
13
|
+
if (authenticateHeader) {
|
|
14
|
+
details.push(`www-authenticate: ${authenticateHeader}`);
|
|
15
|
+
}
|
|
16
|
+
return details.join(" - ");
|
|
17
|
+
}
|
|
18
|
+
async function _readJsonErrorBody(response) {
|
|
19
|
+
const contentType = response.headers.get("content-type")?.split(";")[0];
|
|
20
|
+
if (contentType && contentType !== "application/json" && !contentType.endsWith("+json")) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const body = await response.json();
|
|
25
|
+
if (body && typeof body === "object") {
|
|
26
|
+
return body;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
package/dist/core/_types.d.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
+
export type ManifestKind = "image_index" | "image_manifest" | "artifact_manifest" | "attestation_manifest" | "signature_manifest";
|
|
1
2
|
export interface PackageVersionRecord {
|
|
2
3
|
versionId: number;
|
|
3
|
-
digest: string;
|
|
4
4
|
createdAt: string;
|
|
5
5
|
updatedAt: string;
|
|
6
6
|
metadata?: Record<string, unknown>;
|
|
7
7
|
}
|
|
8
8
|
export interface TagRecord {
|
|
9
9
|
tag: string;
|
|
10
|
-
digest: string;
|
|
11
10
|
versionId: number;
|
|
12
11
|
}
|
|
13
12
|
export interface ManifestRecord {
|
|
13
|
+
versionId: number;
|
|
14
14
|
digest: string;
|
|
15
15
|
mediaType: string;
|
|
16
16
|
artifactType?: string;
|
|
17
17
|
configMediaType?: string;
|
|
18
18
|
subjectDigest?: string;
|
|
19
19
|
annotations?: Record<string, unknown>;
|
|
20
|
+
manifestKind?: ManifestKind;
|
|
20
21
|
platform?: {
|
|
21
22
|
architecture?: string;
|
|
22
23
|
os?: string;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
-
export type { ManifestEdgeRecord, ManifestDescriptorRecord, ManifestRecord, PackageSnapshot, PackageVersionRecord, TagRecord } from "./_types.js";
|
|
1
|
+
export type { ManifestEdgeRecord, ManifestDescriptorRecord, ManifestKind, ManifestRecord, PackageSnapshot, PackageVersionRecord, TagRecord } from "./_types.js";
|
|
2
|
+
export type { HttpErrorResponse } from "./_http-error.js";
|
|
3
|
+
export { buildHttpErrorMessage } from "./_http-error.js";
|
|
4
|
+
export { getOwnerURIComponent } from "./_github-package-owner.js";
|
package/dist/core/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export { buildHttpErrorMessage } from "./_http-error.js";
|
|
2
|
+
export { getOwnerURIComponent } from "./_github-package-owner.js";
|