@mcptoolshop/mcpt-publishing 0.2.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.
@@ -0,0 +1,50 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/mcp-tool-shop/mcpt-publishing/schemas/publishing-config.schema.json",
4
+ "title": "Publishing Config",
5
+ "description": "Configuration for the mcpt-publishing CLI.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string"
11
+ },
12
+ "profilesDir": {
13
+ "type": "string",
14
+ "default": "profiles",
15
+ "description": "Path to profiles directory (relative to config file or absolute)"
16
+ },
17
+ "receiptsDir": {
18
+ "type": "string",
19
+ "default": "receipts",
20
+ "description": "Path to receipts output directory"
21
+ },
22
+ "reportsDir": {
23
+ "type": "string",
24
+ "default": "reports",
25
+ "description": "Path to reports output directory"
26
+ },
27
+ "github": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "properties": {
31
+ "updateIssue": {
32
+ "type": "boolean",
33
+ "default": true,
34
+ "description": "Update the pinned Publishing Health issue after audit"
35
+ },
36
+ "attachReceipts": {
37
+ "type": "boolean",
38
+ "default": true,
39
+ "description": "Attach receipt JSON files to GitHub Releases"
40
+ }
41
+ }
42
+ },
43
+ "enabledProviders": {
44
+ "type": "array",
45
+ "items": { "type": "string" },
46
+ "default": [],
47
+ "description": "Allowlist of provider names. Empty array = all providers enabled."
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,91 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/mcp-tool-shop/mcpt-publishing/schemas/receipt.schema.json",
4
+ "title": "Publish Receipt",
5
+ "description": "Immutable record of a publish event.",
6
+ "type": "object",
7
+ "required": [
8
+ "schemaVersion",
9
+ "repo",
10
+ "target",
11
+ "version",
12
+ "packageName",
13
+ "commitSha",
14
+ "timestamp",
15
+ "artifacts"
16
+ ],
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "schemaVersion": {
20
+ "type": "string",
21
+ "const": "1.0.0",
22
+ "description": "Receipt schema version"
23
+ },
24
+ "repo": {
25
+ "type": "object",
26
+ "required": ["owner", "name"],
27
+ "additionalProperties": false,
28
+ "properties": {
29
+ "owner": { "type": "string" },
30
+ "name": { "type": "string" }
31
+ }
32
+ },
33
+ "target": {
34
+ "type": "string",
35
+ "enum": ["npm", "nuget", "pypi", "ghcr"],
36
+ "description": "Registry this was published to"
37
+ },
38
+ "version": {
39
+ "type": "string",
40
+ "description": "Published version string (e.g. 1.0.0)"
41
+ },
42
+ "packageName": {
43
+ "type": "string",
44
+ "description": "Package name as it appears on the registry"
45
+ },
46
+ "commitSha": {
47
+ "type": "string",
48
+ "pattern": "^[0-9a-f]{40}$",
49
+ "description": "Full 40-char commit SHA at time of publish"
50
+ },
51
+ "timestamp": {
52
+ "type": "string",
53
+ "format": "date-time",
54
+ "description": "ISO 8601 timestamp of publish event"
55
+ },
56
+ "artifacts": {
57
+ "type": "array",
58
+ "items": {
59
+ "type": "object",
60
+ "required": ["name", "sha256", "size", "url"],
61
+ "additionalProperties": false,
62
+ "properties": {
63
+ "name": {
64
+ "type": "string",
65
+ "description": "Artifact filename (e.g. mcpt-1.0.1.tgz)"
66
+ },
67
+ "sha256": {
68
+ "type": "string",
69
+ "pattern": "^[0-9a-f]{64}$",
70
+ "description": "SHA-256 hash of the artifact"
71
+ },
72
+ "size": {
73
+ "type": "integer",
74
+ "minimum": 0,
75
+ "description": "Size in bytes"
76
+ },
77
+ "url": {
78
+ "type": "string",
79
+ "format": "uri",
80
+ "description": "Direct URL to the artifact on the registry"
81
+ }
82
+ }
83
+ }
84
+ },
85
+ "metadata": {
86
+ "type": "object",
87
+ "description": "Optional provider-specific metadata",
88
+ "additionalProperties": true
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * GitHub Glue — connects receipts to GitHub Releases and the health issue.
3
+ *
4
+ * - attachReceiptToRelease() uploads receipt JSON as a release asset
5
+ * - updateHealthIssueWithReceipts() appends "Recent Receipts" to the pinned issue
6
+ */
7
+
8
+ import { execSync } from "node:child_process";
9
+ import { writeFileSync, unlinkSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+
13
+ /**
14
+ * Upload a receipt JSON file as a GitHub Release asset.
15
+ * @param {string} repo - "owner/name"
16
+ * @param {string} tagName - e.g. "v1.0.0"
17
+ * @param {string} receiptPath - absolute path to the receipt JSON file
18
+ * @returns {{ success: boolean, url?: string, error?: string }}
19
+ */
20
+ export function attachReceiptToRelease(repo, tagName, receiptPath) {
21
+ try {
22
+ // Verify the release exists
23
+ execSync(
24
+ `gh api repos/${repo}/releases/tags/${tagName} --jq ".id"`,
25
+ { encoding: "utf8", timeout: 15_000, stdio: ["pipe", "pipe", "pipe"] }
26
+ );
27
+
28
+ // Upload receipt as asset (--clobber overwrites if already attached)
29
+ execSync(
30
+ `gh release upload ${tagName} "${receiptPath}" --repo ${repo} --clobber`,
31
+ { encoding: "utf8", timeout: 30_000, stdio: ["pipe", "pipe", "pipe"] }
32
+ );
33
+
34
+ return { success: true, url: `https://github.com/${repo}/releases/tag/${tagName}` };
35
+ } catch (e) {
36
+ return { success: false, error: e.message };
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Update the pinned "Publishing Health" issue to include receipt links.
42
+ * Appends or replaces a "Recent Receipts" section in the issue body.
43
+ * @param {string} repo - The mcpt-publishing repo
44
+ * @param {Array<{target: string, version: string, packageName: string, releaseUrl: string}>} receipts
45
+ */
46
+ export function updateHealthIssueWithReceipts(repo, receipts) {
47
+ try {
48
+ // Find the pinned health issue
49
+ const issueNum = execSync(
50
+ `gh issue list --repo "${repo}" --search "in:title Publishing Health" --state open --json number --jq ".[0].number"`,
51
+ { encoding: "utf8", timeout: 15_000, stdio: ["pipe", "pipe", "pipe"] }
52
+ ).trim();
53
+
54
+ if (!issueNum) {
55
+ process.stderr.write("Warning: no 'Publishing Health' issue found\n");
56
+ return;
57
+ }
58
+
59
+ // Get current body
60
+ const body = execSync(
61
+ `gh issue view ${issueNum} --repo "${repo}" --json body --jq ".body"`,
62
+ { encoding: "utf8", timeout: 15_000, stdio: ["pipe", "pipe", "pipe"] }
63
+ );
64
+
65
+ // Build receipt links section
66
+ const receiptLines = receipts.map(r =>
67
+ `- **${r.target}** ${r.packageName}@${r.version} — [release](${r.releaseUrl})`
68
+ );
69
+
70
+ const receiptSection = [
71
+ "",
72
+ "### Recent Receipts",
73
+ ...receiptLines,
74
+ "",
75
+ ].join("\n");
76
+
77
+ // Replace existing receipt section or append
78
+ let newBody;
79
+ if (body.includes("### Recent Receipts")) {
80
+ newBody = body.replace(
81
+ /### Recent Receipts[\s\S]*?(?=\n###|\n---|\n## |$)/,
82
+ receiptSection.trim() + "\n"
83
+ );
84
+ } else {
85
+ newBody = body.trimEnd() + "\n" + receiptSection;
86
+ }
87
+
88
+ // Write to temp file to avoid shell escaping issues
89
+ const tmpFile = join(tmpdir(), `health-issue-${Date.now()}.md`);
90
+ writeFileSync(tmpFile, newBody);
91
+
92
+ execSync(
93
+ `gh issue edit ${issueNum} --repo "${repo}" --body-file "${tmpFile}"`,
94
+ { encoding: "utf8", timeout: 15_000, stdio: ["pipe", "pipe", "pipe"] }
95
+ );
96
+
97
+ unlinkSync(tmpFile);
98
+ } catch (e) {
99
+ process.stderr.write(`Warning: failed to update health issue: ${e.message}\n`);
100
+ }
101
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Base class for registry providers.
3
+ *
4
+ * Every provider must extend this and override at least:
5
+ * - get name()
6
+ * - detect(entry)
7
+ * - audit(entry, ctx)
8
+ *
9
+ * publish() and receipt() are optional stubs for future use.
10
+ */
11
+ export class Provider {
12
+ /** Machine-readable name (e.g. "npm", "nuget", "pypi", "ghcr", "github") */
13
+ get name() {
14
+ throw new Error("Provider must define get name()");
15
+ }
16
+
17
+ /**
18
+ * Does this provider apply to the given manifest entry?
19
+ * @param {object} entry - { name, repo, audience, ecosystem }
20
+ * @returns {boolean}
21
+ */
22
+ detect(entry) {
23
+ throw new Error("not implemented");
24
+ }
25
+
26
+ /**
27
+ * Audit a single package. Returns version + findings.
28
+ * @param {object} entry - Manifest entry
29
+ * @param {object} ctx - Shared context { tags: Map, releases: Map }
30
+ * @returns {Promise<{ version: string|null, findings: Array<{severity, code, msg}> }>}
31
+ */
32
+ async audit(entry, ctx) {
33
+ throw new Error("not implemented");
34
+ }
35
+
36
+ /**
37
+ * Plan a publish (dry-run). Override when publish support is added.
38
+ * @param {object} entry
39
+ * @returns {Promise<{ actions: string[] }>}
40
+ */
41
+ async plan(entry) {
42
+ return { actions: [] };
43
+ }
44
+
45
+ /**
46
+ * Execute a publish. Override when publish support is added.
47
+ * @param {object} entry
48
+ * @param {object} opts - { dryRun: boolean, version: string }
49
+ * @returns {Promise<{ success: boolean, version: string, artifacts: object[] }>}
50
+ */
51
+ async publish(entry, opts) {
52
+ throw new Error("not implemented");
53
+ }
54
+
55
+ /**
56
+ * Transform a publish result into a receipt object.
57
+ * @param {object} result - Output from publish()
58
+ * @returns {object} Receipt data conforming to receipt.schema.json
59
+ */
60
+ receipt(result) {
61
+ throw new Error("not implemented");
62
+ }
63
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * GHCR provider — audits container images on GitHub Container Registry.
3
+ *
4
+ * Uses `gh api` to query package versions via the GitHub Packages API.
5
+ */
6
+
7
+ import { execSync } from "node:child_process";
8
+ import { Provider } from "../provider.mjs";
9
+
10
+ export default class GhcrProvider extends Provider {
11
+ get name() { return "ghcr"; }
12
+
13
+ detect(entry) {
14
+ return entry.ecosystem === "ghcr";
15
+ }
16
+
17
+ async audit(entry, ctx) {
18
+ const tags = ctx.tags.get(entry.repo) ?? [];
19
+ const releases = ctx.releases.get(entry.repo) ?? [];
20
+ const versions = this.#fetchVersions(entry.repo, entry.name);
21
+
22
+ if (!versions) {
23
+ return {
24
+ version: "?",
25
+ findings: [{ severity: "RED", code: "ghcr-unreachable", msg: `Cannot reach ${entry.name} on ghcr.io` }],
26
+ };
27
+ }
28
+
29
+ if (versions.length === 0) {
30
+ return {
31
+ version: "?",
32
+ findings: [{ severity: "RED", code: "ghcr-no-versions", msg: `${entry.name} has no versions on ghcr.io` }],
33
+ };
34
+ }
35
+
36
+ // API returns newest first — extract the latest tagged version
37
+ const latest = versions[0];
38
+ const containerTags = latest?.metadata?.container?.tags ?? [];
39
+ const version = containerTags.find(t => /^\d/.test(t)) ?? containerTags[0] ?? "?";
40
+ const findings = this.#classify(entry, version, tags, releases);
41
+
42
+ return { version, findings };
43
+ }
44
+
45
+ // ─── Private ───────────────────────────────────────────────────────────────
46
+
47
+ #fetchVersions(repo, pkg) {
48
+ const owner = repo.split("/")[0];
49
+
50
+ // Try org-level endpoint first
51
+ try {
52
+ const endpoint = `/orgs/${owner}/packages/container/${encodeURIComponent(pkg)}/versions`;
53
+ const raw = execSync(
54
+ `gh api "${endpoint}" --jq "." 2>&1`,
55
+ { encoding: "utf8", timeout: 15_000 }
56
+ );
57
+ return JSON.parse(raw);
58
+ } catch {
59
+ // Fall back to user-level endpoint
60
+ try {
61
+ const endpoint = `/users/${owner}/packages/container/${encodeURIComponent(pkg)}/versions`;
62
+ const raw = execSync(
63
+ `gh api "${endpoint}" --jq "." 2>&1`,
64
+ { encoding: "utf8", timeout: 15_000 }
65
+ );
66
+ return JSON.parse(raw);
67
+ } catch { return null; }
68
+ }
69
+ }
70
+
71
+ #classify(entry, version, tags, releases) {
72
+ const findings = [];
73
+
74
+ if (version === "?") return findings;
75
+
76
+ const tagName = `v${version}`;
77
+
78
+ // Image exists but no matching git tag (RED)
79
+ if (!tags.includes(tagName)) {
80
+ findings.push({
81
+ severity: "RED",
82
+ code: "published-not-tagged",
83
+ msg: `${entry.name} image tagged ${version} — no git tag ${tagName}`,
84
+ });
85
+ }
86
+
87
+ // Tagged-but-not-released (YELLOW for front-door)
88
+ if (tags.includes(tagName) && !releases.includes(tagName) && entry.audience === "front-door") {
89
+ findings.push({
90
+ severity: "YELLOW",
91
+ code: "tagged-not-released",
92
+ msg: `${entry.name} tag ${tagName} has no GitHub Release`,
93
+ });
94
+ }
95
+
96
+ return findings;
97
+ }
98
+
99
+ receipt(result) {
100
+ const [owner, name] = result.repo.split("/");
101
+ return {
102
+ schemaVersion: "1.0.0",
103
+ repo: { owner, name },
104
+ target: "ghcr",
105
+ version: result.version,
106
+ packageName: result.name,
107
+ commitSha: result.commitSha,
108
+ timestamp: new Date().toISOString(),
109
+ artifacts: result.artifacts ?? [],
110
+ };
111
+ }
112
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * GitHub provider — context loader for tags and releases.
3
+ *
4
+ * This provider doesn't produce its own findings. Its job is to populate
5
+ * ctx.tags and ctx.releases so that ecosystem providers (npm, nuget, etc.)
6
+ * can check for drift between registry and git state.
7
+ */
8
+
9
+ import { execSync } from "node:child_process";
10
+ import { Provider } from "../provider.mjs";
11
+
12
+ export default class GitHubProvider extends Provider {
13
+ get name() { return "github"; }
14
+
15
+ detect(entry) {
16
+ return !!entry.repo;
17
+ }
18
+
19
+ async audit(entry, ctx) {
20
+ const repo = entry.repo;
21
+
22
+ if (!ctx.tags.has(repo)) {
23
+ ctx.tags.set(repo, this.#fetchTags(repo));
24
+ }
25
+ if (!ctx.releases.has(repo)) {
26
+ ctx.releases.set(repo, this.#fetchReleases(repo));
27
+ }
28
+
29
+ // No findings — this is a context loader
30
+ return { version: null, findings: [] };
31
+ }
32
+
33
+ #fetchTags(repo) {
34
+ try {
35
+ const raw = execSync(
36
+ `gh api repos/${repo}/tags --jq ".[].name" 2>&1`,
37
+ { encoding: "utf8", timeout: 15_000 }
38
+ );
39
+ return raw.trim().split("\n").filter(Boolean);
40
+ } catch { return []; }
41
+ }
42
+
43
+ #fetchReleases(repo) {
44
+ try {
45
+ const raw = execSync(
46
+ `gh api repos/${repo}/releases --jq ".[].tag_name" 2>&1`,
47
+ { encoding: "utf8", timeout: 15_000 }
48
+ );
49
+ return raw.trim().split("\n").filter(Boolean);
50
+ } catch { return []; }
51
+ }
52
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * npm provider — audit + publish.
3
+ *
4
+ * Audit: fetches metadata via `npm view` and classifies drift against git state.
5
+ * Publish: packs tarball, computes SHA-256, publishes to registry.
6
+ */
7
+
8
+ import { execSync } from "node:child_process";
9
+ import { readFileSync, unlinkSync, existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { Provider } from "../provider.mjs";
12
+ import { exec, hashFile } from "../shell.mjs";
13
+
14
+ export default class NpmProvider extends Provider {
15
+ get name() { return "npm"; }
16
+
17
+ detect(entry) {
18
+ return entry.ecosystem === "npm";
19
+ }
20
+
21
+ async audit(entry, ctx) {
22
+ const meta = this.#fetchMeta(entry.name);
23
+ const tags = ctx.tags.get(entry.repo) ?? [];
24
+ const releases = ctx.releases.get(entry.repo) ?? [];
25
+
26
+ const version = meta?.["dist-tags"]?.latest ?? meta?.version ?? "?";
27
+ const findings = this.#classify(entry, meta, tags, releases);
28
+
29
+ return { version, findings };
30
+ }
31
+
32
+ /**
33
+ * Publish an npm package.
34
+ *
35
+ * @param {object} entry - { name, repo, audience, ecosystem }
36
+ * @param {object} opts - { dryRun: boolean, cwd: string }
37
+ * @returns {Promise<{ success: boolean, version: string, artifacts: Array, error?: string }>}
38
+ */
39
+ async publish(entry, opts = {}) {
40
+ const cwd = opts.cwd ?? process.cwd();
41
+
42
+ // Check credentials (skip in dry-run — npm pack doesn't need auth)
43
+ if (!process.env.NPM_TOKEN && !opts.dryRun) {
44
+ return { success: false, version: "", artifacts: [], error: "NPM_TOKEN environment variable is not set" };
45
+ }
46
+
47
+ // Read version from package.json
48
+ const pkgJsonPath = join(cwd, "package.json");
49
+ if (!existsSync(pkgJsonPath)) {
50
+ return { success: false, version: "", artifacts: [], error: `No package.json found in ${cwd}` };
51
+ }
52
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
53
+ const version = pkgJson.version;
54
+
55
+ if (!version) {
56
+ return { success: false, version: "", artifacts: [], error: "No version field in package.json" };
57
+ }
58
+
59
+ // npm pack → get tarball
60
+ const packResult = exec("npm pack --json", { cwd });
61
+ if (packResult.exitCode !== 0) {
62
+ return { success: false, version, artifacts: [], error: `npm pack failed: ${packResult.stderr}` };
63
+ }
64
+
65
+ let packInfo;
66
+ try {
67
+ const parsed = JSON.parse(packResult.stdout);
68
+ packInfo = Array.isArray(parsed) ? parsed[0] : parsed;
69
+ } catch {
70
+ return { success: false, version, artifacts: [], error: "Failed to parse npm pack output" };
71
+ }
72
+
73
+ const tarball = packInfo.filename;
74
+ const tarballPath = join(cwd, tarball);
75
+
76
+ // Compute SHA-256
77
+ const { sha256, size } = hashFile(tarballPath);
78
+
79
+ // Publish (or dry-run)
80
+ const publishCmd = opts.dryRun
81
+ ? "npm publish --dry-run --access public"
82
+ : "npm publish --access public";
83
+
84
+ const pubResult = exec(publishCmd, { cwd });
85
+ if (pubResult.exitCode !== 0) {
86
+ // Clean up tarball on failure
87
+ try { unlinkSync(tarballPath); } catch { /* ignore */ }
88
+ return { success: false, version, artifacts: [], error: `npm publish failed: ${pubResult.stderr}` };
89
+ }
90
+
91
+ // Clean up tarball
92
+ try { unlinkSync(tarballPath); } catch { /* ignore */ }
93
+
94
+ // Build artifact metadata
95
+ const scopedName = entry.name.replace(/^@/, "").replace(/\//, "-");
96
+ const artifact = {
97
+ name: tarball,
98
+ sha256,
99
+ size,
100
+ url: `https://www.npmjs.com/package/${entry.name}/v/${version}`,
101
+ };
102
+
103
+ return { success: true, version, artifacts: [artifact] };
104
+ }
105
+
106
+ // ─── Private ───────────────────────────────────────────────────────────────
107
+
108
+ #fetchMeta(pkg) {
109
+ try {
110
+ const raw = execSync(`npm view ${pkg} --json 2>&1`, { encoding: "utf8", timeout: 15_000 });
111
+ return JSON.parse(raw);
112
+ } catch { return null; }
113
+ }
114
+
115
+ #classify(pkg, meta, tags, releases) {
116
+ const findings = [];
117
+
118
+ if (!meta) {
119
+ findings.push({ severity: "RED", code: "npm-unreachable", msg: `Cannot reach ${pkg.name} on npm` });
120
+ return findings;
121
+ }
122
+
123
+ const ver = meta["dist-tags"]?.latest ?? meta.version;
124
+ const tagName = `v${ver}`;
125
+
126
+ // Published-but-not-tagged
127
+ if (!tags.includes(tagName)) {
128
+ findings.push({ severity: "RED", code: "published-not-tagged", msg: `${pkg.name}@${ver} — no git tag ${tagName}` });
129
+ }
130
+
131
+ // Tagged-but-not-released
132
+ if (tags.includes(tagName) && !releases.includes(tagName) && pkg.audience === "front-door") {
133
+ findings.push({ severity: "YELLOW", code: "tagged-not-released", msg: `${pkg.name} tag ${tagName} has no GitHub Release` });
134
+ }
135
+
136
+ // Repo URL check
137
+ const repoUrl = meta.repository?.url ?? "";
138
+ if (!repoUrl.includes(pkg.repo.split("/")[0])) {
139
+ findings.push({ severity: "RED", code: "wrong-repo-url", msg: `${pkg.name} repo URL "${repoUrl}" doesn't match expected ${pkg.repo}` });
140
+ }
141
+
142
+ // Description
143
+ const desc = meta.description ?? "";
144
+ if (!desc || desc.startsWith("<") || desc.includes("<img")) {
145
+ findings.push({ severity: "RED", code: "bad-description", msg: `${pkg.name} has missing/HTML description` });
146
+ }
147
+
148
+ // README
149
+ if (meta.readme === "ERROR: No README data found!" || meta.readme === "") {
150
+ if (pkg.audience === "front-door") {
151
+ findings.push({ severity: "YELLOW", code: "missing-readme", msg: `${pkg.name} has no README on npm` });
152
+ } else {
153
+ findings.push({ severity: "GRAY", code: "missing-readme", msg: `${pkg.name} (internal) has no README on npm` });
154
+ }
155
+ }
156
+
157
+ // Homepage
158
+ if (!meta.homepage) {
159
+ findings.push({ severity: "GRAY", code: "missing-homepage", msg: `${pkg.name} has no homepage` });
160
+ }
161
+
162
+ return findings;
163
+ }
164
+
165
+ receipt(result) {
166
+ const [owner, name] = result.repo.split("/");
167
+ return {
168
+ schemaVersion: "1.0.0",
169
+ repo: { owner, name },
170
+ target: "npm",
171
+ version: result.version,
172
+ packageName: result.name,
173
+ commitSha: result.commitSha,
174
+ timestamp: new Date().toISOString(),
175
+ artifacts: result.artifacts ?? [],
176
+ };
177
+ }
178
+ }