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/README.md CHANGED
@@ -77,3 +77,8 @@ OIDC publication and verification that no workflow still uses the credentials.
77
77
  Saved crates.io API sessions run headlessly; sign-in opens a visible window only
78
78
  when user action is needed. Organization migration skips and reports scan errors
79
79
  while continuing for clean repositories.
80
+
81
+ NuGet setup uses an explicit login profile, package owner and exact package IDs.
82
+ Trusted publishing is preferred; existing provided fallback credentials are retained.
83
+ See [audit and NuGet configuration](../docs/issue-73-requirements.md) for setup,
84
+ identity decisions and the distinction between verified configuration and release.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "package-registry-manager",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Inspect repositories and guide secure package-registry setup",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,8 +40,8 @@
40
40
  "audit": "node scripts/npm-audit.mjs"
41
41
  },
42
42
  "dependencies": {
43
- "@link-foundation/gh-manager": "https://codeload.github.com/link-foundation/gh-manager/tar.gz/293e9a8ab9d6d91a08e95f4e09c2a0dcad9bc809",
44
- "browser-commander": "^0.34.0",
43
+ "@link-foundation/gh-manager": "https://codeload.github.com/link-foundation/gh-manager/tar.gz/0b3ceb1590be7eb0340ae088730698b5ae9bebfc",
44
+ "browser-commander": "^0.34.1",
45
45
  "command-stream": "^2.6.0",
46
46
  "playwright": "^1.63.0",
47
47
  "selenium-webdriver": "^4.50.0"
@@ -0,0 +1,96 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /** Explicit source decisions supplement static discovery without executing build files. */
5
+ export async function auditConfig(root) {
6
+ const config = JSON.parse(
7
+ await readFile(
8
+ path.join(root, ".package-registry-manager.json"),
9
+ "utf8",
10
+ ).catch((error) => {
11
+ if (error.code === "ENOENT") {
12
+ return "{}";
13
+ }
14
+ throw error;
15
+ }),
16
+ );
17
+ if (
18
+ config.publish !== undefined &&
19
+ (!Array.isArray(config.publish) ||
20
+ config.publish.some(
21
+ (file) =>
22
+ typeof file !== "string" ||
23
+ path.posix.normalize(file) !== file ||
24
+ file.startsWith("/") ||
25
+ file.split("/").includes(".."),
26
+ ))
27
+ ) {
28
+ throw new Error(
29
+ "publish must list exact repository-relative manifest paths",
30
+ );
31
+ }
32
+ return config;
33
+ }
34
+
35
+ /** A reviewed mapping chooses one canonical artifact; references never publish a different tarball. */
36
+ export function applyAuditConfig(items, config, workflows) {
37
+ for (const item of items.filter(Boolean)) {
38
+ const gradle = config.gradle?.find(
39
+ (entry) => entry.manifest === item.manifest,
40
+ );
41
+ if (gradle) {
42
+ if (
43
+ ![
44
+ gradle.group,
45
+ gradle.artifact,
46
+ gradle.version,
47
+ gradle.component,
48
+ ].every((value) => typeof value === "string" && /^[\w.-]+$/.test(value))
49
+ ) {
50
+ throw new Error(
51
+ "explicit Gradle publication requires resolved group, artifact, version and component",
52
+ );
53
+ }
54
+ Object.assign(item, {
55
+ registry: "maven-central",
56
+ name: `${gradle.group}:${gradle.artifact}`,
57
+ version: gradle.version,
58
+ publishable: true,
59
+ publication_component: gradle.component,
60
+ });
61
+ delete item.discovery_skip;
62
+ }
63
+ const shared = config.shared_artifacts?.find(
64
+ (entry) =>
65
+ `${entry.registry}:${entry.name}` === `${item.registry}:${item.name}`,
66
+ );
67
+ if (shared) {
68
+ item.shared_artifact = shared;
69
+ }
70
+ if (item.distribution) {
71
+ item.distribution = {
72
+ ...item.distribution,
73
+ ...config.distribution?.[item.manifest],
74
+ workflows: workflows
75
+ .filter((workflow) =>
76
+ item.registry === "conan"
77
+ ? /conan\s+upload/.test(workflow.contents)
78
+ : item.registry === "vcpkg"
79
+ ? /vcpkg.*(?:x-add-version|registry|publish)/.test(
80
+ workflow.contents,
81
+ )
82
+ : /cpack|cmake.*--install/.test(workflow.contents),
83
+ )
84
+ .map((workflow) => ({
85
+ workflow: workflow.name,
86
+ remote_variable: /CONAN_REMOTE_URL/.test(workflow.contents)
87
+ ? "CONAN_REMOTE_URL"
88
+ : undefined,
89
+ credential_names: ["CONAN_LOGIN_USERNAME", "CONAN_PASSWORD"].filter(
90
+ (name) => workflow.contents.includes(name),
91
+ ),
92
+ })),
93
+ };
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,163 @@
1
+ import { probePackage } from "./registry-state.mjs";
2
+ import {
3
+ publisherIdentities,
4
+ publisherMatches,
5
+ repositorySlug,
6
+ } from "./repository-identity.mjs";
7
+
8
+ /** Bootstrap is a repaired prerequisite, separate from OIDC rejection. */
9
+ export function bootstrapNames(log) {
10
+ const names = [];
11
+ for (const match of log.matchAll(
12
+ /npm owner setup required for ([^:\n]+):/gi,
13
+ )) {
14
+ for (const name of match[1].split(/[,\s]+/).filter(Boolean)) {
15
+ if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(name)) {
16
+ return [];
17
+ }
18
+ names.push(name);
19
+ }
20
+ }
21
+ return [...new Set(names)];
22
+ }
23
+
24
+ /** Fresh name and exact publisher checks are equivalent to the owner-setup preflight. */
25
+ export async function verifyBootstrapRecovery(session) {
26
+ if (!session.bootstrapNames?.length) {
27
+ return;
28
+ }
29
+ for (const name of session.bootstrapNames) {
30
+ const state = await (session.options.probePackage ?? probePackage)(
31
+ { ...session.plan.package, name },
32
+ session.options,
33
+ );
34
+ if (state.exists !== true) {
35
+ throw new Error(`bootstrap preflight still unresolved for ${name}`);
36
+ }
37
+ const trust = session.plan.steps?.find(
38
+ (step) => step.id === "verify-trusted-publisher",
39
+ )?.command;
40
+ const args = trust
41
+ ? trust.args.map((argument) =>
42
+ argument === session.plan.package.name ? name : argument,
43
+ )
44
+ : ["-y", "npm@^11.15", "trust", "list", name, "--browser=false"];
45
+ const result = await session.capture({
46
+ command: { program: trust?.program ?? "npx", args },
47
+ });
48
+ if (
49
+ result.code !== 0 ||
50
+ !publisherIdentities(String(result.stdout)).some((publisher) =>
51
+ publisherMatches(publisher, session.plan.trusted_publisher),
52
+ )
53
+ ) {
54
+ throw new Error(`bootstrap publisher is not verified for ${name}`);
55
+ }
56
+ }
57
+ const slug = repositorySlug(session.plan.repository);
58
+ const result = await session.capture({
59
+ command: {
60
+ program: "gh",
61
+ args: [
62
+ "api",
63
+ `repos/${slug}/commits/${session.releaseSelection.branch}`,
64
+ "--jq",
65
+ ".sha",
66
+ ],
67
+ },
68
+ cwd: ".",
69
+ });
70
+ if (
71
+ result.code !== 0 ||
72
+ String(result.stdout).trim() !== session.releaseSelection.run.headSha
73
+ ) {
74
+ throw new Error(
75
+ "default branch changed; follow the new revision's release instead of rerunning the old SHA",
76
+ );
77
+ }
78
+ }
79
+
80
+ /** Retry only jobs whose own failed log contains the repaired prerequisite. */
81
+ export async function recoverBootstrap(session) {
82
+ await verifyBootstrapRecovery(session);
83
+ const selection = session.releaseSelection;
84
+ const slug = repositorySlug(session.plan.repository);
85
+ const jobIds = [];
86
+ for (const job of selection.jobs ?? []) {
87
+ if (job.conclusion !== "failure") {
88
+ continue;
89
+ }
90
+ const log = await session.capture({
91
+ command: {
92
+ program: "gh",
93
+ args: [
94
+ "run",
95
+ "view",
96
+ String(selection.run.databaseId),
97
+ "--repo",
98
+ slug,
99
+ "--job",
100
+ String(job.databaseId),
101
+ "--log",
102
+ ],
103
+ },
104
+ cwd: ".",
105
+ });
106
+ if (log.code !== 0) {
107
+ throw new Error(
108
+ "causal failed-job logs are unavailable; no CI was restarted",
109
+ );
110
+ }
111
+ if (
112
+ bootstrapNames(String(log.stdout)).some((name) =>
113
+ session.bootstrapNames.includes(name),
114
+ )
115
+ ) {
116
+ jobIds.push(job.databaseId);
117
+ }
118
+ }
119
+ if (!jobIds.length) {
120
+ throw new Error(
121
+ "no causal bootstrap job could be verified; unrelated failed jobs were not retried",
122
+ );
123
+ }
124
+ const changes =
125
+ session.options.recoveryChanges?.get(
126
+ `${slug}:${session.plan.trusted_publisher.workflow}`,
127
+ ) ?? [];
128
+ if (!changes.length) {
129
+ throw new Error(
130
+ "no verified configuration change in this setup; CI was not restarted",
131
+ );
132
+ }
133
+ const { githubServices } = await import("./github.mjs");
134
+ const github = session.options.github ?? githubServices(session.options);
135
+ const result = await github.repair.verify({
136
+ repo: slug,
137
+ workflow: session.plan.trusted_publisher.workflow,
138
+ runId: selection.run.databaseId,
139
+ jobIds,
140
+ changes: changes.map(() => ({
141
+ kind: "setting",
142
+ changed: true,
143
+ verified: true,
144
+ })),
145
+ timeout: session.options.waitTimeoutMs ?? 600000,
146
+ });
147
+ console.log(JSON.stringify({ bootstrap_recovery: result }));
148
+ session.conditions.delete("release-dispatch");
149
+ session.values.run_id = String(result.runId);
150
+ if (result.status !== "success") {
151
+ throw new Error(
152
+ `repaired bootstrap CI remains ${result.status}; ${result.runUrl ?? selection.run.url}`,
153
+ );
154
+ }
155
+ const { verifyNpmRelease } = await import("./npm-release.mjs");
156
+ const packages = await verifyNpmRelease(session, result.runId);
157
+ session.outcome = {
158
+ status: "verified",
159
+ url: result.runUrl,
160
+ ci: result,
161
+ packages,
162
+ };
163
+ }
@@ -0,0 +1,164 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /** Read distribution metadata without executing a CMake file or Python recipe. */
5
+ export async function cppDistribution(
6
+ contents,
7
+ filename,
8
+ manifest,
9
+ manifestPath,
10
+ ) {
11
+ let cmake = filename === "CMakeLists.txt" ? contents : "";
12
+ if (!cmake) {
13
+ try {
14
+ cmake = await readFile(
15
+ path.join(path.dirname(manifestPath), "CMakeLists.txt"),
16
+ "utf8",
17
+ );
18
+ } catch (error) {
19
+ if (error.code !== "ENOENT") {
20
+ throw error;
21
+ }
22
+ }
23
+ }
24
+ const project =
25
+ /\bproject\s*\(\s*([\w.-]+)\s+VERSION\s+(\d+(?:\.\d+)+|\$\{(\w+)\})/i.exec(
26
+ cmake,
27
+ );
28
+ let name = project?.[1];
29
+ let version = project?.[3] ? null : project?.[2];
30
+ let versionSource = null;
31
+ if (project?.[3]) {
32
+ // `file(READ "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" VAR)` is resolved by
33
+ // reading that one sibling file; nothing in the CMake file is executed.
34
+ const read = new RegExp(
35
+ `\\bfile\\s*\\(\\s*READ\\s+"?\\$\\{CMAKE_CURRENT_(?:SOURCE|LIST)_DIR\\}/([\\w.-]+)"?\\s+${project[3]}\\s*\\)`,
36
+ "i",
37
+ ).exec(cmake);
38
+ const literal = new RegExp(
39
+ `\\bset\\s*\\(\\s*${project[3]}\\s+"?(\\d+(?:\\.\\d+)+)"?\\s*\\)`,
40
+ "i",
41
+ ).exec(cmake);
42
+ if (read) {
43
+ try {
44
+ const value = (
45
+ await readFile(path.join(path.dirname(manifestPath), read[1]), "utf8")
46
+ ).trim();
47
+ if (/^\d+(?:\.\d+)+$/.test(value)) {
48
+ version = value;
49
+ versionSource = path.posix.join(
50
+ path.posix.dirname(manifest),
51
+ read[1],
52
+ );
53
+ }
54
+ } catch (error) {
55
+ if (error.code !== "ENOENT") {
56
+ throw error;
57
+ }
58
+ }
59
+ } else if (literal) {
60
+ version = literal[1];
61
+ }
62
+ }
63
+ const registry =
64
+ filename === "vcpkg.json"
65
+ ? "vcpkg"
66
+ : filename === "conanfile.py"
67
+ ? "conan"
68
+ : "cmake";
69
+ if (registry === "vcpkg") {
70
+ const data = JSON.parse(contents);
71
+ name = data.name;
72
+ version =
73
+ data.version ??
74
+ data["version-semver"] ??
75
+ data["version-string"] ??
76
+ data["version-date"];
77
+ } else if (registry === "conan") {
78
+ name = /^\s*name\s*=\s*["']([^"']+)/m.exec(contents)?.[1] ?? name;
79
+ version = /^\s*version\s*=\s*["']([^"']+)/m.exec(contents)?.[1] ?? version;
80
+ }
81
+ if (
82
+ !name ||
83
+ (registry === "cmake" &&
84
+ !/install\s*\(|CPack|configure_package_config_file/i.test(contents))
85
+ ) {
86
+ return {
87
+ manifest,
88
+ discovery_skip:
89
+ "CMake build definition has no distribution installation/configuration",
90
+ publishable: false,
91
+ };
92
+ }
93
+ return {
94
+ registry,
95
+ name,
96
+ version: version ?? null,
97
+ manifest,
98
+ publishable: true,
99
+ distribution: {
100
+ version_source:
101
+ versionSource ??
102
+ (project
103
+ ? path.posix.join(path.posix.dirname(manifest), "CMakeLists.txt")
104
+ : manifest),
105
+ provider_status: "unverified",
106
+ ...(registry === "conan"
107
+ ? {
108
+ remote_required: "CONAN_REMOTE_URL",
109
+ credential_names: ["CONAN_LOGIN_USERNAME", "CONAN_PASSWORD"],
110
+ }
111
+ : {}),
112
+ },
113
+ warnings: [
114
+ registry === "conan"
115
+ ? "Conan remote configuration and publication remain unverified; a local recipe does not establish registry presence"
116
+ : `${registry} distribution metadata does not establish a public registry publication`,
117
+ ],
118
+ };
119
+ }
120
+
121
+ /** Validation and explicit channel prerequisites; no upload is performed. */
122
+ export function cppPlan(item) {
123
+ const commands = {
124
+ cmake: {
125
+ program: "cmake",
126
+ args: ["-S", ".", "-B", "build/prm-validation"],
127
+ },
128
+ conan: { program: "conan", args: ["inspect", ".", "--format=json"] },
129
+ vcpkg: {
130
+ program: "vcpkg",
131
+ args: ["install", "--dry-run", "--x-manifest-root=."],
132
+ },
133
+ };
134
+ return [
135
+ {
136
+ id: "validate-package",
137
+ title: `Validate ${item.registry} distribution`,
138
+ kind: "check",
139
+ command: commands[item.registry],
140
+ description:
141
+ "Validate the manifest locally before reviewing any publication settings.",
142
+ },
143
+ ...(item.registry === "conan"
144
+ ? [
145
+ {
146
+ id: "audit-distribution-channel",
147
+ title: "Read Conan remote and credential health",
148
+ kind: "api",
149
+ description:
150
+ "Read effective remote configuration and credential health through gh-manager. Unknown credentials and missing remotes remain explicit; do not upload or rotate credentials.",
151
+ },
152
+ ]
153
+ : []),
154
+ {
155
+ id: "review-distribution-channel",
156
+ title: "Resolve the distribution provider and CI configuration",
157
+ kind: "manual",
158
+ description:
159
+ item.registry === "conan"
160
+ ? "Read CONAN_REMOTE_URL and credential health for CONAN_LOGIN_USERNAME/CONAN_PASSWORD through gh-manager at the effective repository/environment scope. Choose the exact remote, validate conan create and consumer tests in reviewed CI, then configure narrowly scoped existing credentials. No public registry state or permission is established by a recipe."
161
+ : `Review ${item.registry === "vcpkg" ? "the chosen vcpkg registry repository, baseline, port and version database" : "CMake install/export, package config, CPack and GitHub Release workflow"}; validate installation and a consumer in CI. Provider lookup/setup is unsupported until the destination is explicitly configured; publication remains unverified.`,
162
+ },
163
+ ];
164
+ }
@@ -1,8 +1,13 @@
1
1
  /** Registry-specific policy; tokens are never a fallback for OIDC registries. */
2
2
  export function credentialPolicy(registry) {
3
- if (
4
- ["npm", "pypi", "crates-io", "rubygems", "nuget", "jsr"].includes(registry)
5
- ) {
3
+ if (registry === "nuget") {
4
+ return {
5
+ mode: "oidc-with-token-fallback",
6
+ description:
7
+ "NuGet: NuGet/login OIDC first, retain provided fallback secrets; never create a persistent publishing key",
8
+ };
9
+ }
10
+ if (["npm", "pypi", "crates-io", "rubygems", "jsr"].includes(registry)) {
6
11
  return {
7
12
  mode: "trusted",
8
13
  description: `${registry}: trusted publishing; remove unused long-lived tokens after verification`,
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Registries without public OIDC publishing: the exact credential set a
3
+ * workflow needs, which secrets it already references, and where values may
4
+ * live. Values are only ever held in memory or written to encrypted GitHub
5
+ * secrets; nothing here rotates, revokes or deletes an existing credential.
6
+ */
7
+ export const CREDENTIAL_SETS = Object.freeze({
8
+ "maven-central": {
9
+ oidc: "unsupported",
10
+ oidc_note:
11
+ "Sonatype Central Portal has no OIDC trusted publishing; publish with a Central user-token pair and a GPG signing key.",
12
+ namespace: "verified Central Portal namespace matching the groupId",
13
+ roles: [
14
+ {
15
+ role: "central-username",
16
+ candidates: [
17
+ "CENTRAL_USERNAME",
18
+ "MAVEN_CENTRAL_USERNAME",
19
+ "OSSRH_USERNAME",
20
+ "SONATYPE_USERNAME",
21
+ "MAVEN_USERNAME",
22
+ ],
23
+ default: "CENTRAL_USERNAME",
24
+ },
25
+ {
26
+ role: "central-token",
27
+ candidates: [
28
+ "CENTRAL_TOKEN",
29
+ "MAVEN_CENTRAL_TOKEN",
30
+ "MAVEN_CENTRAL_PASSWORD",
31
+ "OSSRH_TOKEN",
32
+ "OSSRH_PASSWORD",
33
+ "SONATYPE_TOKEN",
34
+ "MAVEN_PASSWORD",
35
+ ],
36
+ default: "CENTRAL_TOKEN",
37
+ },
38
+ {
39
+ role: "gpg-private-key",
40
+ candidates: ["GPG_PRIVATE_KEY", "MAVEN_GPG_PRIVATE_KEY", "SIGNING_KEY"],
41
+ default: "GPG_PRIVATE_KEY",
42
+ },
43
+ {
44
+ role: "gpg-passphrase",
45
+ candidates: [
46
+ "GPG_PASSPHRASE",
47
+ "MAVEN_GPG_PASSPHRASE",
48
+ "SIGNING_PASSWORD",
49
+ ],
50
+ default: "GPG_PASSPHRASE",
51
+ },
52
+ ],
53
+ },
54
+ packagist: {
55
+ oidc: "unsupported",
56
+ oidc_note:
57
+ "Public packagist.org has no OIDC publishing (Private Packagist OIDC is a separate product); releases come from tags via the GitHub hook or the update-package API.",
58
+ roles: [
59
+ {
60
+ role: "packagist-username",
61
+ candidates: ["PACKAGIST_USERNAME"],
62
+ default: "PACKAGIST_USERNAME",
63
+ optional: true,
64
+ },
65
+ {
66
+ role: "packagist-api-token",
67
+ candidates: ["PACKAGIST_TOKEN", "PACKAGIST_API_TOKEN"],
68
+ default: "PACKAGIST_TOKEN",
69
+ optional: true,
70
+ },
71
+ ],
72
+ },
73
+ });
74
+
75
+ const SECRET = /\bsecrets\.([A-Za-z_][A-Za-z0-9_]*)\b/g;
76
+
77
+ /** Structured requirements resolved against the secrets the workflows read. */
78
+ export function credentialRequirements(registry, workflows = []) {
79
+ const set = CREDENTIAL_SETS[registry];
80
+ if (!set) {
81
+ return null;
82
+ }
83
+ const referenced = new Set(
84
+ workflows.flatMap((workflow) =>
85
+ [...workflow.contents.replace(/^\s*#.*$/gm, "").matchAll(SECRET)].map(
86
+ (match) => match[1],
87
+ ),
88
+ ),
89
+ );
90
+ return {
91
+ oidc: set.oidc,
92
+ oidc_note: set.oidc_note,
93
+ ...(set.namespace ? { namespace: set.namespace } : {}),
94
+ storage: "memory-or-encrypted-github-secret",
95
+ secrets: set.roles.map((role) => {
96
+ const found = role.candidates.find((name) => referenced.has(name));
97
+ return {
98
+ role: role.role,
99
+ secret: found ?? role.default,
100
+ referenced: Boolean(found),
101
+ ...(role.optional ? { optional: true } : {}),
102
+ };
103
+ }),
104
+ };
105
+ }
@@ -1,4 +1,4 @@
1
- import { exec } from "command-stream";
1
+ import { exec } from "./exec.mjs";
2
2
 
3
3
  import { browserSources, catalogueName } from "./browser-catalogue.mjs";
4
4
 
@@ -1,4 +1,4 @@
1
- import { exec } from "command-stream";
1
+ import { exec } from "./exec.mjs";
2
2
 
3
3
  async function github(args) {
4
4
  try {