package-registry-manager 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +5 -0
  2. package/package.json +3 -3
  3. package/src/audit-config.mjs +96 -0
  4. package/src/bootstrap-recovery.mjs +163 -0
  5. package/src/cpp-distribution.mjs +164 -0
  6. package/src/credential-cycle.mjs +8 -3
  7. package/src/credential-requirements.mjs +105 -0
  8. package/src/default-browser.mjs +1 -1
  9. package/src/default-scope.mjs +1 -1
  10. package/src/discovery.mjs +289 -24
  11. package/src/distribution-channel.mjs +87 -0
  12. package/src/exec.mjs +20 -0
  13. package/src/extra-flows.mjs +122 -2
  14. package/src/flows.mjs +19 -3
  15. package/src/generated-artifact.mjs +48 -0
  16. package/src/github.mjs +4 -0
  17. package/src/identity-conflicts.mjs +130 -0
  18. package/src/manifest-proposal.mjs +1 -1
  19. package/src/model.mjs +6 -0
  20. package/src/npm-release.mjs +68 -0
  21. package/src/nuget-ci.mjs +238 -0
  22. package/src/nuget-migration.mjs +275 -0
  23. package/src/nuget-page.mjs +100 -0
  24. package/src/nuget-policy.mjs +198 -0
  25. package/src/nuget-publish.py +145 -0
  26. package/src/nuget-setup.mjs +169 -0
  27. package/src/nuget-workflow.mjs +54 -0
  28. package/src/organization-scan.mjs +41 -2
  29. package/src/pack-review.mjs +122 -0
  30. package/src/package-coverage.mjs +33 -9
  31. package/src/plan.mjs +35 -9
  32. package/src/prerequisites.mjs +1 -1
  33. package/src/profile.mjs +1 -1
  34. package/src/publication-state.mjs +299 -0
  35. package/src/publishers.mjs +78 -12
  36. package/src/publishing-policy.mjs +7 -1
  37. package/src/publishing-state.mjs +74 -1
  38. package/src/python.mjs +1 -1
  39. package/src/registry-state.mjs +118 -3
  40. package/src/release-runs.mjs +3 -4
  41. package/src/setup-batch.mjs +48 -2
  42. package/src/setup.mjs +82 -120
  43. package/src/sign-in-import.mjs +132 -40
  44. package/src/skips.mjs +9 -1
  45. package/src/source-code.mjs +1 -1
  46. package/src/tokens.mjs +32 -4
  47. package/src/workflow-migration.mjs +15 -2
  48. package/src/workflow-proposal.mjs +6 -2
package/src/discovery.mjs CHANGED
@@ -1,3 +1,7 @@
1
+ import { credentialRequirements } from "./credential-requirements.mjs";
2
+ import { auditConfig, applyAuditConfig } from "./audit-config.mjs";
3
+ import { cppDistribution } from "./cpp-distribution.mjs";
4
+ import { markIdentityConflicts } from "./identity-conflicts.mjs";
1
5
  import { readFile, readdir, realpath } from "node:fs/promises";
2
6
  import path from "node:path";
3
7
 
@@ -6,6 +10,7 @@ import { packageWorkflows, auditWrappers } from "./package-coverage.mjs";
6
10
  import { REGISTRIES } from "./model.mjs";
7
11
  import { inspectManifestRepositories } from "./repository-identity.mjs";
8
12
  import {
13
+ RELEASE_EVIDENCE,
9
14
  TRUSTED_REGISTRIES,
10
15
  detectPublisher,
11
16
  tokenSecretWarning,
@@ -32,6 +37,9 @@ const IGNORED_DIRECTORIES = new Set([
32
37
  ]);
33
38
 
34
39
  const MANIFEST_NAMES = new Set([
40
+ "CMakeLists.txt",
41
+ "conanfile.py",
42
+ "vcpkg.json",
35
43
  "package.json",
36
44
  "Cargo.toml",
37
45
  "pyproject.toml",
@@ -63,6 +71,7 @@ export async function inspectRepository(repository, options = {}) {
63
71
  );
64
72
  const workflows = await readWorkflows(root);
65
73
  const ignore = await readIgnoreList(root);
74
+ const config = await auditConfig(root);
66
75
  const skipped = [];
67
76
  const skip = (manifest, reason) => {
68
77
  skipped.push({ manifest, reason });
@@ -77,7 +86,10 @@ export async function inspectRepository(repository, options = {}) {
77
86
  const directory = testDirectory(manifest);
78
87
  return (
79
88
  directory === null ||
80
- (!["docs", "experiments", "case-studies"].includes(directory) &&
89
+ config.publish?.includes(manifest) ||
90
+ (!["docs", "experiments", "case-studies", "dev/log"].includes(
91
+ directory,
92
+ ) &&
81
93
  publishable &&
82
94
  referencedByWorkflow(workflows, manifest)) ||
83
95
  skip(manifest, testDirectoryReason(directory))
@@ -89,7 +101,7 @@ export async function inspectRepository(repository, options = {}) {
89
101
  .filter(([, manifest]) => !ignored(manifest))
90
102
  .map(async ([manifestPath, manifest]) => {
91
103
  try {
92
- return await parseManifest(manifestPath, manifest);
104
+ return await parseManifest(manifestPath, manifest, root);
93
105
  } catch (error) {
94
106
  if (testDirectory(manifest) === null) {
95
107
  if (!options.onError) {
@@ -106,6 +118,7 @@ export async function inspectRepository(repository, options = {}) {
106
118
  }
107
119
  }),
108
120
  );
121
+ applyAuditConfig(parsed, config, workflows);
109
122
  const coordinates = await githubCoordinates(
110
123
  root,
111
124
  preferredManifests.filter((manifest) => {
@@ -117,6 +130,56 @@ export async function inspectRepository(repository, options = {}) {
117
130
  for (const registry of TRUSTED_REGISTRIES) {
118
131
  publishers.set(registry, await detectPublisher(root, workflows, registry));
119
132
  }
133
+ // Generated names share source metadata, but are independent registry identities.
134
+ for (const item of [...parsed].filter(Boolean)) {
135
+ if (item.registry !== "npm") {
136
+ continue;
137
+ }
138
+ const data = JSON.parse(await readManifest(path.join(root, item.manifest)));
139
+ const aliases = data.publishAliases ?? [];
140
+ if (
141
+ !Array.isArray(aliases) ||
142
+ aliases.some(
143
+ (name) =>
144
+ typeof name !== "string" ||
145
+ !/^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(name),
146
+ ) ||
147
+ new Set([item.name, ...aliases]).size !== aliases.length + 1
148
+ ) {
149
+ throw new Error(
150
+ `${item.manifest}: publishAliases must contain distinct valid npm names`,
151
+ );
152
+ }
153
+ const generator =
154
+ Object.values(data.scripts ?? {}).flatMap((command) =>
155
+ [
156
+ ...String(command).matchAll(
157
+ /(?:^|\s)(?:node|bun)\s+(?:\.\/)?([^\s]+prepare[^\s]*\.mjs)/g,
158
+ ),
159
+ ].map((match) => match[1]),
160
+ )[0] ?? null;
161
+ if (aliases.length) {
162
+ item.generated_artifact = {
163
+ source_manifest: item.manifest,
164
+ source_name: item.name,
165
+ generator,
166
+ names: [item.name, ...aliases],
167
+ };
168
+ }
169
+ for (const name of aliases) {
170
+ parsed.push({
171
+ ...item,
172
+ name,
173
+ generated_alias: {
174
+ source_manifest: item.manifest,
175
+ source_name: item.name,
176
+ generator,
177
+ names: [item.name, ...aliases],
178
+ },
179
+ });
180
+ }
181
+ }
182
+ applyAuditConfig(parsed, config, workflows);
120
183
  // Extension packages can publish to both marketplaces independently.
121
184
  for (const item of [...parsed].filter(Boolean)) {
122
185
  if (item.registry !== "npm") {
@@ -137,10 +200,10 @@ export async function inspectRepository(repository, options = {}) {
137
200
  }
138
201
  }
139
202
  }
140
- publishers.set(
141
- "maven-central",
142
- await detectPublisher(root, workflows, "maven-central"),
143
- );
203
+ for (const registry of SCOPED_REGISTRIES) {
204
+ publishers.set(registry, await detectPublisher(root, workflows, registry));
205
+ }
206
+ canonicalComposer(parsed);
144
207
  const containerFiles = dockerfiles
145
208
  .map((item) => relativePath(root, item))
146
209
  .filter((item) => !ignored(item) && kept(item, true));
@@ -169,11 +232,16 @@ export async function inspectRepository(repository, options = {}) {
169
232
  await Promise.all(
170
233
  parsed
171
234
  .filter(Boolean)
172
- .filter((item) => kept(item.manifest, item.publishable))
235
+ .filter((item) =>
236
+ item.discovery_skip
237
+ ? skip(item.manifest, item.discovery_skip)
238
+ : kept(item.manifest, item.publishable),
239
+ )
173
240
  .map(async (item) => {
174
241
  const scoped =
175
- counts.get(item.registry) > 1 &&
176
- TRUSTED_REGISTRIES.includes(item.registry)
242
+ (counts.get(item.registry) > 1 &&
243
+ TRUSTED_REGISTRIES.includes(item.registry)) ||
244
+ SCOPED_REGISTRIES.includes(item.registry)
177
245
  ? new Map([
178
246
  [
179
247
  item.registry,
@@ -185,10 +253,37 @@ export async function inspectRepository(repository, options = {}) {
185
253
  ],
186
254
  ])
187
255
  : publishers;
188
- return withPublisher(item, workflows, scoped);
256
+ return withPublisher(
257
+ item,
258
+ workflows,
259
+ item.publisher_scope
260
+ ? new Map([
261
+ [
262
+ item.registry,
263
+ await detectPublisher(
264
+ root,
265
+ packageWorkflows(workflows, {
266
+ ...item,
267
+ manifest: item.publisher_scope,
268
+ }),
269
+ item.registry,
270
+ ),
271
+ ],
272
+ ])
273
+ : scoped,
274
+ );
189
275
  }),
190
276
  )
191
277
  )
278
+ .filter(
279
+ (item) =>
280
+ !internalWorkload(item) ||
281
+ config.publish?.includes(item.manifest) ||
282
+ skip(
283
+ item.manifest,
284
+ `internal ${internalWorkload(item)} workload: no workflow publishes it and no source decision lists it under "publish"`,
285
+ ),
286
+ )
192
287
  .concat(
193
288
  containerPackages(containerFiles, workflows, coordinates),
194
289
  chromePackages,
@@ -223,6 +318,7 @@ export async function inspectRepository(repository, options = {}) {
223
318
  left.manifest.localeCompare(right.manifest),
224
319
  );
225
320
  }
321
+ markIdentityConflicts([inspection]);
226
322
  return inspectManifestRepositories(inspection);
227
323
  }
228
324
 
@@ -240,6 +336,47 @@ function releaseWorkflow(publishers) {
240
336
  return null;
241
337
  }
242
338
 
339
+ /** Registries whose publication evidence is scoped to one manifest. */
340
+ const SCOPED_REGISTRIES = ["maven-central", "packagist", "go-modules", "cmake"];
341
+
342
+ /** Benchmark/fixture manifests are internal unless a workflow releases them. */
343
+ function internalWorkload(item) {
344
+ if (item.workflow) {
345
+ return null;
346
+ }
347
+ const segment = item.manifest
348
+ .split("/")
349
+ .slice(0, -1)
350
+ .find((part) => /^(?:bench(?:mark)?(?:es|s)?|perf)$/i.test(part));
351
+ return segment ? "benchmark" : null;
352
+ }
353
+
354
+ /**
355
+ * Packagist reads only the repository-root composer.json, so a same-name
356
+ * descriptor in a subdirectory is a development copy of that distribution.
357
+ * The root keeps the copy's declared version and the copy's publishing job.
358
+ */
359
+ function canonicalComposer(parsed) {
360
+ const composer = parsed.filter(
361
+ (item) => item?.registry === "packagist" && !item.discovery_skip,
362
+ );
363
+ const root = composer.find((item) => item.manifest === "composer.json");
364
+ if (!root) {
365
+ return;
366
+ }
367
+ for (const copy of composer.filter(
368
+ (item) => item !== root && item.name === root.name,
369
+ )) {
370
+ copy.discovery_skip = `Packagist reads only the repository-root composer.json; ${copy.manifest} is a development descriptor of ${root.name}`;
371
+ copy.publishable = false;
372
+ if (!root.version && copy.version) {
373
+ root.version = copy.version;
374
+ root.version_source = copy.manifest;
375
+ }
376
+ root.publisher_scope ??= copy.manifest;
377
+ }
378
+ }
379
+
243
380
  function withPublisher(item, workflows, publishers) {
244
381
  const publisher = item.publishable ? publishers.get(item.registry) : null;
245
382
  if (!publisher) {
@@ -255,16 +392,32 @@ function withPublisher(item, workflows, publishers) {
255
392
  if (publisher.jobs.length > 0) {
256
393
  result.workflow_jobs = publisher.jobs;
257
394
  }
395
+ if (publisher.callees?.length) {
396
+ // Reusable workflows publish under the caller's identity; the callee
397
+ // jobs are kept so setup and recovery act on the actual publishing job.
398
+ result.workflow_callees = publisher.callees;
399
+ }
400
+ if (publisher.gate) {
401
+ result.authentication_gate = publisher.gate;
402
+ }
258
403
  if (publisher.environment) {
259
404
  result.environment = publisher.environment;
260
405
  }
261
406
  if (publisher.candidates.length > 1) {
262
407
  result.workflow_candidates = publisher.candidates;
263
408
  }
409
+ const credentials = credentialRequirements(item.registry, workflows);
410
+ if (credentials) {
411
+ result.credential_requirements = credentials;
412
+ }
264
413
  const secrets = tokenSecrets(workflows, item.registry);
265
414
  const warnings = [
266
415
  ...publisher.warnings,
267
- ...(!publisher.workflow && publisher.candidates.length === 0
416
+ // Tags and registry hooks release Go modules, Packagist packages and
417
+ // CMake archives without a workflow, so only evidence is reported.
418
+ ...(!publisher.workflow &&
419
+ publisher.candidates.length === 0 &&
420
+ !RELEASE_EVIDENCE.has(item.registry)
268
421
  ? [
269
422
  `no workflow publishes ${item.name} to ${item.registry}; CI releases will not reach it`,
270
423
  ]
@@ -301,10 +454,14 @@ async function collectManifests(directory, manifests, dockerfiles) {
301
454
  }
302
455
  }
303
456
 
304
- async function parseManifest(manifestPath, manifest) {
457
+ async function parseManifest(manifestPath, manifest, root) {
305
458
  const contents = await readManifest(manifestPath);
306
459
  const filename = path.basename(manifestPath);
307
460
  switch (filename) {
461
+ case "CMakeLists.txt":
462
+ case "conanfile.py":
463
+ case "vcpkg.json":
464
+ return cppDistribution(contents, filename, manifest, manifestPath);
308
465
  case "jsr.json":
309
466
  case "deno.json": {
310
467
  const value = JSON.parse(contents);
@@ -315,7 +472,10 @@ async function parseManifest(manifestPath, manifest) {
315
472
  case "package.json":
316
473
  return parseNpm(contents, manifest);
317
474
  case "Cargo.toml":
318
- return parseCargo(contents, manifest);
475
+ return parseCargo(
476
+ await cargoContents(contents, manifestPath, root),
477
+ manifest,
478
+ );
319
479
  case "pyproject.toml":
320
480
  return parsePyproject(contents, manifest);
321
481
  case "setup.py":
@@ -326,13 +486,22 @@ async function parseManifest(manifestPath, manifest) {
326
486
  capture(contents, /\bversion\s*=\s*['"]([^'"]+)/),
327
487
  manifest,
328
488
  );
329
- case "go.mod":
330
- return packageInfo(
489
+ case "go.mod": {
490
+ const item = packageInfo(
331
491
  "go-modules",
332
492
  capture(contents, /^\s*module\s+(\S+)/m) ?? "unknown-go-module",
333
- null,
493
+ await versionFile(manifestPath),
334
494
  manifest,
335
495
  );
496
+ if (item.version) {
497
+ item.version = `v${item.version.replace(/^v/, "")}`;
498
+ item.version_source = path.posix.join(
499
+ path.posix.dirname(manifest),
500
+ "VERSION",
501
+ );
502
+ }
503
+ return item;
504
+ }
336
505
  case "pom.xml":
337
506
  return parseMaven(contents, manifest);
338
507
  case "build.gradle":
@@ -373,6 +542,48 @@ function parseNpm(contents, manifest) {
373
542
  );
374
543
  }
375
544
 
545
+ async function cargoContents(contents, manifestPath, root) {
546
+ if (
547
+ !/version\s*\.\s*workspace\s*=\s*true|version\s*=\s*\{\s*workspace\s*=\s*true/.test(
548
+ contents,
549
+ )
550
+ ) {
551
+ return contents;
552
+ }
553
+ const section = tomlSection(contents, "package") ?? "";
554
+ const explicit = tomlString(section, "workspace");
555
+ let directory = explicit
556
+ ? path.resolve(path.dirname(manifestPath), explicit)
557
+ : path.dirname(manifestPath);
558
+ while (directory === root || directory.startsWith(`${root}${path.sep}`)) {
559
+ let workspace;
560
+ try {
561
+ workspace = await readManifest(path.join(directory, "Cargo.toml"));
562
+ } catch (error) {
563
+ if (error.code !== "ENOENT") {
564
+ throw error;
565
+ }
566
+ }
567
+ if (workspace && tomlSection(workspace, "workspace") !== null) {
568
+ const version = tomlString(
569
+ tomlSection(workspace, "workspace.package") ?? "",
570
+ "version",
571
+ );
572
+ return version
573
+ ? contents.replace(
574
+ /version\s*\.\s*workspace\s*=\s*true|version\s*=\s*\{\s*workspace\s*=\s*true\s*\}/,
575
+ `version = "${version}"`,
576
+ )
577
+ : contents;
578
+ }
579
+ if (explicit || directory === root) {
580
+ break;
581
+ }
582
+ directory = path.dirname(directory);
583
+ }
584
+ return contents;
585
+ }
586
+
376
587
  function parseCargo(contents, manifest) {
377
588
  const section = tomlSection(contents, "package");
378
589
  if (!section) {
@@ -414,16 +625,38 @@ function parsePyproject(contents, manifest) {
414
625
  }
415
626
 
416
627
  function parseDotnet(contents, manifestPath, manifest) {
628
+ if (xmlTag(contents, "IsPackable")?.toLowerCase() === "false") {
629
+ return {
630
+ manifest,
631
+ publishable: false,
632
+ discovery_skip: "MSBuild IsPackable is false",
633
+ };
634
+ }
417
635
  return packageInfo(
418
636
  "nuget",
419
637
  xmlTag(contents, "PackageId") ??
420
638
  xmlTag(contents, "AssemblyName") ??
421
639
  path.basename(manifestPath, path.extname(manifestPath)),
422
- xmlTag(contents, "PackageVersion") ?? xmlTag(contents, "Version"),
640
+ dotnetVersion(contents),
423
641
  manifest,
424
642
  );
425
643
  }
426
644
 
645
+ function dotnetVersion(contents) {
646
+ const literal = (tag) => {
647
+ const value = xmlTag(contents, tag);
648
+ return value && !value.includes("$(") ? value : null;
649
+ };
650
+ const explicit = literal("PackageVersion") ?? literal("Version");
651
+ if (explicit) {
652
+ return explicit;
653
+ }
654
+ // MSBuild derives Version from VersionPrefix and VersionSuffix.
655
+ const prefix = literal("VersionPrefix");
656
+ const suffix = literal("VersionSuffix");
657
+ return prefix ? (suffix ? `${prefix}-${suffix}` : prefix) : null;
658
+ }
659
+
427
660
  function parseMaven(contents, manifest) {
428
661
  const artifact = xmlTag(contents, "artifactId") ?? "unknown-maven-artifact";
429
662
  const group = xmlTag(contents, "groupId");
@@ -436,16 +669,33 @@ function parseMaven(contents, manifest) {
436
669
  }
437
670
 
438
671
  function parseGradle(contents, manifest) {
439
- const group = capture(contents, /^\s*group\s*=\s*['"]([^'"]+)/m);
672
+ const literal = (key) =>
673
+ capture(contents, new RegExp(`\\b${key}\\s*(?:=\\s*)?['"]([^'"]+)`));
674
+ const group = literal("groupId") ?? literal("group");
440
675
  const artifact =
441
- capture(
442
- contents,
443
- /^\s*(?:archivesBaseName|rootProject\.name)\s*=\s*['"]([^'"]+)/m,
444
- ) ?? "gradle-project";
676
+ literal("artifactId") ??
677
+ literal("archivesBaseName") ??
678
+ literal("rootProject\\.name");
679
+ const version = literal("version");
680
+ if (
681
+ !group ||
682
+ !artifact ||
683
+ !version ||
684
+ /[$}{]/.test(`${group}${artifact}${version}`) ||
685
+ !/maven-publish|MavenPublication/.test(contents) ||
686
+ !/from\s*\(?\s*components|artifact\s*\(/.test(contents)
687
+ ) {
688
+ return {
689
+ manifest,
690
+ publishable: false,
691
+ discovery_skip:
692
+ "Gradle application/build definition has no resolved Maven publication coordinates and component",
693
+ };
694
+ }
445
695
  return packageInfo(
446
696
  "maven-central",
447
- group ? `${group}:${artifact}` : artifact,
448
- capture(contents, /^\s*version\s*=\s*['"]([^'"]+)/m),
697
+ `${group}:${artifact}`,
698
+ version,
449
699
  manifest,
450
700
  );
451
701
  }
@@ -484,6 +734,21 @@ function packageInfo(
484
734
  return result;
485
735
  }
486
736
 
737
+ /** A plain sibling VERSION file, as release tooling for Go/C++ often keeps. */
738
+ export async function versionFile(manifestPath) {
739
+ try {
740
+ const value = (
741
+ await readManifest(path.join(path.dirname(manifestPath), "VERSION"))
742
+ ).trim();
743
+ return /^v?\d+(?:\.\d+){1,3}(?:[-+][\w.-]+)?$/.test(value) ? value : null;
744
+ } catch (error) {
745
+ if (error.code === "ENOENT") {
746
+ return null;
747
+ }
748
+ throw error;
749
+ }
750
+ }
751
+
487
752
  // Editors on Windows may save a byte order mark; npm and Cargo accept it.
488
753
  async function readManifest(manifest) {
489
754
  return (await readFile(manifest, "utf8")).replace(/^\uFEFF/, "");
@@ -0,0 +1,87 @@
1
+ import { githubServices } from "./github.mjs";
2
+ import { repositorySlug } from "./repository-identity.mjs";
3
+
4
+ /** GitHub configuration is evidence about a channel, never public registry presence. */
5
+ export async function auditDistributionChannel(session) {
6
+ const distribution = session.plan.package.distribution ?? {};
7
+ const scope = {
8
+ repo: repositorySlug(session.plan.repository),
9
+ ...(distribution.environment
10
+ ? { environment: distribution.environment }
11
+ : {}),
12
+ };
13
+ const github = session.options.github ?? githubServices(session.options);
14
+ const configured = distribution.remote
15
+ ? { verified: true, value: distribution.remote }
16
+ : await github.variables(scope).effective("CONAN_REMOTE_URL");
17
+ let remote = { status: configured.verified ? "missing" : "unknown" };
18
+ if (configured.value) {
19
+ try {
20
+ const url = new URL(configured.value);
21
+ remote = {
22
+ status:
23
+ configured.verified &&
24
+ ["https:", "http:"].includes(url.protocol) &&
25
+ !url.username &&
26
+ !url.password &&
27
+ !url.search &&
28
+ !url.hash
29
+ ? "configured"
30
+ : "invalid-or-secret-bearing",
31
+ };
32
+ } catch {
33
+ remote = { status: "invalid" };
34
+ }
35
+ }
36
+ const credentials = [];
37
+ for (const name of ["CONAN_LOGIN_USERNAME", "CONAN_PASSWORD"]) {
38
+ try {
39
+ const result = await github.health.health(name, {
40
+ scope: { repo: scope.repo },
41
+ failurePatterns: ["conan.*(?:unauthorized|forbidden|authentication)"],
42
+ });
43
+ credentials.push({
44
+ name,
45
+ status: result.status,
46
+ evidence: (result.evidence ?? []).map(
47
+ ({ reason, status, workflow, job, step, runId, runUrl }) => ({
48
+ reason,
49
+ status,
50
+ workflow,
51
+ job,
52
+ step,
53
+ runId,
54
+ runUrl,
55
+ }),
56
+ ),
57
+ ...(scope.environment
58
+ ? {
59
+ limitation:
60
+ "health is repository workflow evidence; effective environment credentials require the selected CI job",
61
+ }
62
+ : {}),
63
+ });
64
+ } catch {
65
+ credentials.push({
66
+ name,
67
+ status: "unknown",
68
+ reason: "credential-health-unreadable",
69
+ });
70
+ }
71
+ }
72
+ const result = {
73
+ provider: "conan",
74
+ scope,
75
+ remote,
76
+ credentials,
77
+ publication: "unverified",
78
+ ci_validation: [
79
+ "conan create . --build=missing",
80
+ "run the recipe test_package consumer with the reviewed profiles",
81
+ "verify the exact reference on the chosen remote after reviewed CI publication",
82
+ ],
83
+ };
84
+ session.outcome = { status: "unverified", distribution: result };
85
+ console.log(JSON.stringify(result));
86
+ return result;
87
+ }
package/src/exec.mjs ADDED
@@ -0,0 +1,20 @@
1
+ import { execa } from "command-stream/execa";
2
+
3
+ /** Capture an exact argument vector, including Windows PATHEXT launchers. */
4
+ export async function exec(program, args, options = {}) {
5
+ const output = options.mirror ? ["pipe", "inherit"] : "pipe";
6
+ const result = await execa(program, args, {
7
+ cwd: options.cwd,
8
+ env: options.env,
9
+ stdin: options.stdin ?? "ignore",
10
+ stdout: output,
11
+ stderr: output,
12
+ reject: false,
13
+ stripFinalNewline: false,
14
+ });
15
+ return {
16
+ code: result.exitCode ?? 1,
17
+ stdout: result.stdout ?? "",
18
+ stderr: result.stderr ?? "",
19
+ };
20
+ }