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.
- package/README.md +5 -0
- package/package.json +3 -3
- package/src/audit-config.mjs +96 -0
- package/src/bootstrap-recovery.mjs +163 -0
- package/src/cpp-distribution.mjs +164 -0
- package/src/credential-cycle.mjs +8 -3
- package/src/credential-requirements.mjs +105 -0
- package/src/default-browser.mjs +1 -1
- package/src/default-scope.mjs +1 -1
- package/src/discovery.mjs +289 -24
- package/src/distribution-channel.mjs +87 -0
- package/src/exec.mjs +20 -0
- package/src/extra-flows.mjs +122 -2
- package/src/flows.mjs +19 -3
- package/src/generated-artifact.mjs +48 -0
- package/src/github.mjs +4 -0
- package/src/identity-conflicts.mjs +130 -0
- package/src/manifest-proposal.mjs +1 -1
- package/src/model.mjs +6 -0
- package/src/npm-release.mjs +68 -0
- package/src/nuget-ci.mjs +238 -0
- package/src/nuget-migration.mjs +275 -0
- package/src/nuget-page.mjs +100 -0
- package/src/nuget-policy.mjs +198 -0
- package/src/nuget-publish.py +145 -0
- package/src/nuget-setup.mjs +169 -0
- package/src/nuget-workflow.mjs +54 -0
- package/src/organization-scan.mjs +41 -2
- package/src/pack-review.mjs +122 -0
- package/src/package-coverage.mjs +33 -9
- package/src/plan.mjs +35 -9
- package/src/prerequisites.mjs +1 -1
- package/src/profile.mjs +1 -1
- package/src/publication-state.mjs +299 -0
- package/src/publishers.mjs +78 -12
- package/src/publishing-policy.mjs +7 -1
- package/src/publishing-state.mjs +74 -1
- package/src/python.mjs +1 -1
- package/src/registry-state.mjs +118 -3
- package/src/release-runs.mjs +3 -4
- package/src/setup-batch.mjs +48 -2
- package/src/setup.mjs +82 -120
- package/src/sign-in-import.mjs +132 -40
- package/src/skips.mjs +9 -1
- package/src/source-code.mjs +1 -1
- package/src/tokens.mjs +32 -4
- package/src/workflow-migration.mjs +15 -2
- package/src/workflow-proposal.mjs +6 -2
package/src/nuget-ci.mjs
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { githubServices } from "./github.mjs";
|
|
2
|
+
import { repositorySlug } from "./repository-identity.mjs";
|
|
3
|
+
import { AUTH_FAILURE_PATTERNS } from "./publishing-policy.mjs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/** Job names (or reusable-call segments) whose skip would hide the release. */
|
|
7
|
+
export function requiredJobs(pkg = {}) {
|
|
8
|
+
return [
|
|
9
|
+
...(pkg.workflow_jobs ?? []),
|
|
10
|
+
...(pkg.workflow_callees ?? []).map((chain) => chain.split(" -> ").at(-1)),
|
|
11
|
+
].filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Failed or cancelled jobs always block. A skipped job blocks only when it is a
|
|
16
|
+
* required release job (or when the required set is unknown); PR-only reviews
|
|
17
|
+
* and unchanged-component jobs legitimately skip on a push to main.
|
|
18
|
+
*/
|
|
19
|
+
export function unmetJobs(jobs, required) {
|
|
20
|
+
const isRequired = (job) =>
|
|
21
|
+
!required.length ||
|
|
22
|
+
job.name.split(" / ").some((segment) => required.includes(segment.trim()));
|
|
23
|
+
const unmet = jobs.filter(
|
|
24
|
+
(job) =>
|
|
25
|
+
!["success", "skipped"].includes(job.conclusion) ||
|
|
26
|
+
(job.conclusion === "skipped" && isRequired(job)),
|
|
27
|
+
);
|
|
28
|
+
if (
|
|
29
|
+
required.length &&
|
|
30
|
+
!jobs.some((job) => isRequired(job) && job.conclusion === "success")
|
|
31
|
+
) {
|
|
32
|
+
unmet.push({ name: required.join(", "), conclusion: "missing" });
|
|
33
|
+
}
|
|
34
|
+
return unmet;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Fresh main-revision CI, actual method and exact public consumers are separate gates. */
|
|
38
|
+
export async function verifyNugetRelease(session) {
|
|
39
|
+
const github = session.options.github ?? githubServices(session.options);
|
|
40
|
+
const slug = repositorySlug(session.plan.repository);
|
|
41
|
+
const publisher = session.plan.trusted_publisher;
|
|
42
|
+
const repo = await github.rest.request(`/repos/${slug}`);
|
|
43
|
+
const revision = await github.rest.request(
|
|
44
|
+
`/repos/${slug}/commits/${encodeURIComponent(repo.default_branch)}`,
|
|
45
|
+
);
|
|
46
|
+
const histories = await Promise.all(
|
|
47
|
+
["push", "workflow_dispatch"].map((event) =>
|
|
48
|
+
github.runs.list(slug, {
|
|
49
|
+
workflow: publisher.workflow,
|
|
50
|
+
branch: repo.default_branch,
|
|
51
|
+
event,
|
|
52
|
+
limit: 100,
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
const runs = histories
|
|
57
|
+
.flat()
|
|
58
|
+
.filter(
|
|
59
|
+
(run) =>
|
|
60
|
+
run.head_branch === repo.default_branch &&
|
|
61
|
+
["push", "workflow_dispatch"].includes(run.event),
|
|
62
|
+
)
|
|
63
|
+
.sort((a, b) => b.id - a.id);
|
|
64
|
+
const current = runs.find((run) => run.head_sha === revision.sha);
|
|
65
|
+
const failed = runs.find((run) => run.conclusion === "failure");
|
|
66
|
+
const changes =
|
|
67
|
+
session.options.recoveryChanges?.get(`${slug}:${publisher.workflow}`) ?? [];
|
|
68
|
+
let result;
|
|
69
|
+
if (failed && changes.length && current?.conclusion !== "success") {
|
|
70
|
+
const jobs = await github.runs.jobs(slug, failed.id);
|
|
71
|
+
const causal = [];
|
|
72
|
+
const auth = new RegExp(AUTH_FAILURE_PATTERNS.nuget.join("|"), "i");
|
|
73
|
+
for (const job of jobs.filter((job) => job.conclusion === "failure")) {
|
|
74
|
+
const log = await github.runs.jobLog(slug, job.id);
|
|
75
|
+
if (
|
|
76
|
+
typeof log === "string" &&
|
|
77
|
+
auth.test(log) &&
|
|
78
|
+
/nuget|NuGet\/login/i.test(log)
|
|
79
|
+
) {
|
|
80
|
+
causal.push(job.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!causal.length) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"NuGet settings are verified but no causal authentication failure was established; unrelated CI was not restarted",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (failed.head_sha !== revision.sha && !current) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"NuGet code repair has no run at current main; merge the reviewed workflow and follow its push run",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
result = await github.repair.verify({
|
|
94
|
+
repo: slug,
|
|
95
|
+
workflow: publisher.workflow,
|
|
96
|
+
runId: failed.id,
|
|
97
|
+
jobIds: causal,
|
|
98
|
+
requiredPermissions: (session.plan.package.workflow_jobs ?? []).map(
|
|
99
|
+
(job) => ({ job, permission: "id-token", level: "write" }),
|
|
100
|
+
),
|
|
101
|
+
changes: [
|
|
102
|
+
{ kind: "setting", changed: true, verified: true },
|
|
103
|
+
...(failed.head_sha !== revision.sha
|
|
104
|
+
? [{ kind: "workflow", changed: true, verified: true }]
|
|
105
|
+
: []),
|
|
106
|
+
],
|
|
107
|
+
...(failed.head_sha !== revision.sha
|
|
108
|
+
? {
|
|
109
|
+
revision: { ref: repo.default_branch, sha: revision.sha },
|
|
110
|
+
newRunId: current.id,
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
113
|
+
timeout: session.options.waitTimeoutMs ?? 600000,
|
|
114
|
+
});
|
|
115
|
+
console.log(JSON.stringify({ nuget_ci: result }));
|
|
116
|
+
if (result.status !== "success") {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`NuGet repaired CI remains ${result.status}; ${result.runUrl ?? failed.html_url}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const id = result?.runId ?? current?.id;
|
|
123
|
+
if (!id) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"NuGet current-main release has not executed; PR checks and settings readback do not verify publication",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const deadline = Date.now() + (session.options.waitTimeoutMs ?? 600000);
|
|
129
|
+
let run;
|
|
130
|
+
for (;;) {
|
|
131
|
+
run = await github.runs.get(slug, id);
|
|
132
|
+
if (run.status === "completed") {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (Date.now() >= deadline) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`NuGet current-main pipeline remains pending: ${run.html_url}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
await new Promise((resolve) =>
|
|
141
|
+
setTimeout(resolve, session.options.pollIntervalMs ?? 5000),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const jobs = await github.runs.jobs(slug, id);
|
|
145
|
+
if (
|
|
146
|
+
run.head_sha !== revision.sha ||
|
|
147
|
+
run.status !== "completed" ||
|
|
148
|
+
run.conclusion !== "success" ||
|
|
149
|
+
!jobs.length ||
|
|
150
|
+
unmetJobs(jobs, requiredJobs(session.plan.package)).length
|
|
151
|
+
) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`NuGet current-main pipeline has incomplete gates: ${run.html_url}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const methods = [];
|
|
157
|
+
for (const job of jobs) {
|
|
158
|
+
const source = await github.runs.jobLog(slug, job.id);
|
|
159
|
+
if (typeof source !== "string") {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
for (const line of source.split("\n")) {
|
|
163
|
+
const start = line.indexOf('{"selected_method"');
|
|
164
|
+
if (start < 0) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const evidence = JSON.parse(line.slice(start));
|
|
169
|
+
if (
|
|
170
|
+
["trusted-publishing", "token-fallback"].includes(
|
|
171
|
+
evidence.selected_method,
|
|
172
|
+
) &&
|
|
173
|
+
evidence.publication === "accepted" &&
|
|
174
|
+
evidence.package_id?.toLowerCase() ===
|
|
175
|
+
session.plan.package.name.toLowerCase() &&
|
|
176
|
+
/^[0-9][0-9a-z.+-]*$/i.test(evidence.version ?? "")
|
|
177
|
+
) {
|
|
178
|
+
methods.push({ job: job.name, ...evidence });
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
/* Invalid or unavailable logs cannot establish a method. */
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!methods.length) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
"NuGet authentication method and actual upload are unverified; use the reviewed publication controller in current-main CI",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const fresh = await github.rest.request(
|
|
191
|
+
`/repos/${slug}/commits/${encodeURIComponent(repo.default_branch)}`,
|
|
192
|
+
);
|
|
193
|
+
if (fresh.sha !== run.head_sha) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
"main advanced during NuGet verification; follow its new run before claiming completion",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const versions = [...new Set(methods.map((method) => method.version))];
|
|
199
|
+
if (versions.length !== 1) {
|
|
200
|
+
throw new Error("NuGet actual released version is ambiguous");
|
|
201
|
+
}
|
|
202
|
+
const released = { ...session.plan.package, version: versions[0] };
|
|
203
|
+
if (session.options.verifyNugetPublic) {
|
|
204
|
+
await session.options.verifyNugetPublic(released);
|
|
205
|
+
} else {
|
|
206
|
+
const checked = await session.capture({
|
|
207
|
+
command: {
|
|
208
|
+
program: "python3",
|
|
209
|
+
args: [
|
|
210
|
+
fileURLToPath(new URL("./nuget-publish.py", import.meta.url)),
|
|
211
|
+
"--verify-public",
|
|
212
|
+
session.plan.package.name,
|
|
213
|
+
released.version,
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
cwd: ".",
|
|
217
|
+
});
|
|
218
|
+
if (checked.code !== 0) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
"exact NuGet public download and consumer restore/build remain unverified",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
session.outcome = {
|
|
225
|
+
status: "verified",
|
|
226
|
+
url: run.html_url,
|
|
227
|
+
ci: {
|
|
228
|
+
run_id: id,
|
|
229
|
+
attempt: run.run_attempt,
|
|
230
|
+
sha: run.head_sha,
|
|
231
|
+
jobs,
|
|
232
|
+
authentication: methods,
|
|
233
|
+
},
|
|
234
|
+
public_version: released.version,
|
|
235
|
+
consumer: "passed",
|
|
236
|
+
};
|
|
237
|
+
console.log(JSON.stringify(session.outcome));
|
|
238
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { nugetWorkflowSteps } from "./nuget-workflow.mjs";
|
|
2
|
+
|
|
3
|
+
export const NUGET_PREREQUISITE_EDIT =
|
|
4
|
+
"Split the NuGet publish step so one step runs only `dotnet nuget push <artifact glob> --api-key ... --source https://api.nuget.org/v3/index.json` (optionally behind a `should_publish` guard, `${A:-$B}` key selection, a key-presence check and `published=` outputs); pack, tests and any other command must stay in earlier steps.";
|
|
5
|
+
|
|
6
|
+
function prerequisite(reason) {
|
|
7
|
+
const error = new Error(
|
|
8
|
+
`${reason}. Prerequisite edit: ${NUGET_PREREQUISITE_EDIT}`,
|
|
9
|
+
);
|
|
10
|
+
error.code = "nuget-prerequisite-edit";
|
|
11
|
+
error.prerequisite = NUGET_PREREQUISITE_EDIT;
|
|
12
|
+
return error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Recognize the guarded multiline push used by links-notation style workflows.
|
|
17
|
+
* Every line must be the guard, key selection, presence check, notices,
|
|
18
|
+
* `published=` outputs or one nuget.org push; anything else is not migrated.
|
|
19
|
+
*/
|
|
20
|
+
export function guardedNugetPush(step) {
|
|
21
|
+
const start = step.findIndex((line) =>
|
|
22
|
+
/^\s*-?\s*run:\s*\|[-+]?\s*$/.test(line),
|
|
23
|
+
);
|
|
24
|
+
if (start < 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const level = step[start].search(/\S/);
|
|
28
|
+
const body = [];
|
|
29
|
+
for (const line of step.slice(start + 1)) {
|
|
30
|
+
if (line.trim() && line.search(/\S/) <= level) {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
body.push(line.trim());
|
|
34
|
+
}
|
|
35
|
+
const joined = body.join("\n").replace(/\\\n\s*/g, " ");
|
|
36
|
+
const lines = joined
|
|
37
|
+
.split("\n")
|
|
38
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
39
|
+
let guard = null;
|
|
40
|
+
let push = null;
|
|
41
|
+
let block = null;
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const opened =
|
|
44
|
+
/^if \[ "\$\{\{\s*([\w.-]+)\s*\}\}" != "true" \]; then$/.exec(line);
|
|
45
|
+
if (opened && !block && !guard) {
|
|
46
|
+
guard = opened[1];
|
|
47
|
+
block = "guard";
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (/^if \[ -z "\$\w+" \]; then$/.test(line) && !block) {
|
|
51
|
+
block = "missing";
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (line === "fi" && block) {
|
|
55
|
+
block = null;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (block === "guard" && /^exit 0$/.test(line)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (block === "missing" && /^exit 1$/.test(line)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
/^set -[euxo pipefail]+$/.test(line) ||
|
|
66
|
+
/^\w+="\$\{\w+:-\$\w+\}"$/.test(line) ||
|
|
67
|
+
/^echo "[^"`$]*(?:\$\{\{[^}]*\}\}[^"`$]*)*"(?: >> "\$GITHUB_OUTPUT")?$/.test(
|
|
68
|
+
line,
|
|
69
|
+
)
|
|
70
|
+
) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const command =
|
|
74
|
+
/^dotnet nuget push (?:"([^"]+)"|'([^']+)'|(\S+))(.*)$/.exec(line);
|
|
75
|
+
if (command && !push && !block) {
|
|
76
|
+
push = command;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
throw prerequisite(
|
|
80
|
+
`NuGet publish script contains an unreviewed command: ${line}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (!push || block) {
|
|
84
|
+
throw prerequisite(
|
|
85
|
+
"NuGet multiline step has no single recognizable artifact push",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
guard,
|
|
90
|
+
glob: push[1] ?? push[2] ?? push[3],
|
|
91
|
+
options: push[4],
|
|
92
|
+
start,
|
|
93
|
+
level,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Preserve validation, build, artifact and step conditions when replacing a direct push. */
|
|
98
|
+
export function convertNugetStep(step, packages, index) {
|
|
99
|
+
if (packages.length !== 1) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"NuGet migration requires a package-specific publishing step; ambiguous multi-package globs need explicit review",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const guarded = guardedNugetPush(step);
|
|
105
|
+
if (guarded) {
|
|
106
|
+
return convertGuarded(step, packages, index, guarded);
|
|
107
|
+
}
|
|
108
|
+
const run = step.find((line) => /^\s*-?\s*run:/.test(line));
|
|
109
|
+
const push =
|
|
110
|
+
/^\s*-?\s*run:\s*dotnet\s+nuget\s+push\s+(?:"([^"\n]+)"|'([^'\n]+)'|([^\s]+))\s*(.*)$/.exec(
|
|
111
|
+
run ?? "",
|
|
112
|
+
);
|
|
113
|
+
if (!push || /[;\n`]|\$\(/.test(push.slice(1).join(""))) {
|
|
114
|
+
throw prerequisite(
|
|
115
|
+
"NuGet script or multiline push requires a reviewed artifact-only publishing command; validation/build commands cannot be retried",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const glob = push[1] ?? push[2] ?? push[3];
|
|
119
|
+
if (!/^[\w./*${}-]+$/.test(glob) || glob.includes("${{")) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"NuGet artifact glob must resolve from job-local environment variables",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
/--source\s+(?!["']?https:\/\/api\.nuget\.org\/v3\/index\.json(?:["']?\s|$))/.test(
|
|
126
|
+
push[4],
|
|
127
|
+
)
|
|
128
|
+
) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"NuGet policy migration requires the explicit nuget.org destination",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const custom = [...step.join("\n").matchAll(/secrets\.([\w]+)/g)].map(
|
|
134
|
+
(match) => match[1],
|
|
135
|
+
);
|
|
136
|
+
const output = nugetWorkflowSteps(
|
|
137
|
+
{
|
|
138
|
+
...packages[0],
|
|
139
|
+
token_secrets: [...custom, ...(packages[0].token_secrets ?? [])],
|
|
140
|
+
},
|
|
141
|
+
{ build: false, artifactGlob: glob },
|
|
142
|
+
);
|
|
143
|
+
const prefix = " ".repeat(step[0].search(/\S/));
|
|
144
|
+
const lines = output
|
|
145
|
+
.map((line) => prefix + line.slice(6))
|
|
146
|
+
.map((line) =>
|
|
147
|
+
line.replaceAll("prm_nuget_login", `prm_nuget_login_${index}`),
|
|
148
|
+
);
|
|
149
|
+
const condition = step.find((line) => /^\s*if:/.test(line));
|
|
150
|
+
const directory = step.find((line) => /^\s*working-directory:/.test(line));
|
|
151
|
+
const publish = lines.findIndex((line) =>
|
|
152
|
+
/- name: Publish validated NuGet/.test(line),
|
|
153
|
+
);
|
|
154
|
+
if (condition) {
|
|
155
|
+
const expression = condition
|
|
156
|
+
.trim()
|
|
157
|
+
.slice(3)
|
|
158
|
+
.trim()
|
|
159
|
+
.replace(/^\$\{\{\s*|\s*\}\}$/g, "");
|
|
160
|
+
const loginIf = lines.findIndex((line) => /if: vars.NUGET_USER/.test(line));
|
|
161
|
+
lines[loginIf] += ` && (${expression})`;
|
|
162
|
+
lines.splice(publish + 1, 0, `${prefix} if: ${expression}`);
|
|
163
|
+
}
|
|
164
|
+
if (directory) {
|
|
165
|
+
lines.splice(
|
|
166
|
+
lines.findIndex((line) => /- name: Publish validated NuGet/.test(line)) +
|
|
167
|
+
1,
|
|
168
|
+
0,
|
|
169
|
+
`${prefix} ${directory.trim()}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const envIndex = step.findIndex((line) => /^\s*env:/.test(line));
|
|
173
|
+
if (envIndex >= 0) {
|
|
174
|
+
if (!/^\s*env:\s*$/.test(step[envIndex])) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
"NuGet migration requires a resolved step environment mapping",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const level = step[envIndex].search(/\S/);
|
|
180
|
+
const entries = [];
|
|
181
|
+
for (const line of step.slice(envIndex + 1)) {
|
|
182
|
+
if (line.trim() && line.search(/\S/) <= level) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
if (/^\s*PRM_\w+:/.test(line)) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
"NuGet migration cannot overwrite a publication controller environment",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
entries.push(line);
|
|
191
|
+
}
|
|
192
|
+
const env = lines.findIndex((line) => /^\s*env:\s*$/.test(line));
|
|
193
|
+
lines.splice(env + 1, 0, ...entries);
|
|
194
|
+
}
|
|
195
|
+
return lines;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Guard -> OIDC login -> controller push of the same artifacts -> original outputs. */
|
|
199
|
+
function convertGuarded(step, packages, index, guarded) {
|
|
200
|
+
if (
|
|
201
|
+
/--source\s+(?!["']?https:\/\/api\.nuget\.org\/v3\/index\.json)/.test(
|
|
202
|
+
guarded.options,
|
|
203
|
+
)
|
|
204
|
+
) {
|
|
205
|
+
throw prerequisite(
|
|
206
|
+
"NuGet policy migration requires the explicit nuget.org destination",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (!/^[\w./*-]+$/.test(guarded.glob)) {
|
|
210
|
+
throw prerequisite("NuGet artifact glob must be a literal job-local path");
|
|
211
|
+
}
|
|
212
|
+
const prefix = " ".repeat(step[0].search(/\S/));
|
|
213
|
+
const header = step.slice(0, guarded.start);
|
|
214
|
+
const name =
|
|
215
|
+
/-\s*name:\s*(.+)$/.exec(step[0])?.[1] ?? "Publish NuGet package";
|
|
216
|
+
const id = header
|
|
217
|
+
.map((line) => /^\s*id:\s*(\S+)/.exec(line)?.[1])
|
|
218
|
+
.find(Boolean);
|
|
219
|
+
const condition = header
|
|
220
|
+
.map((line) => /^\s*if:\s*(.+)$/.exec(line)?.[1])
|
|
221
|
+
.find(Boolean)
|
|
222
|
+
?.replace(/^\$\{\{\s*|\s*\}\}$/g, "");
|
|
223
|
+
const secrets = [...step.join("\n").matchAll(/secrets\.(\w+)/g)].map(
|
|
224
|
+
(m) => m[1],
|
|
225
|
+
);
|
|
226
|
+
const gates = [condition, guarded.guard && `${guarded.guard} == 'true'`]
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
.map((part) => `(${part})`)
|
|
229
|
+
.join(" && ");
|
|
230
|
+
const publishId = `prm_nuget_publish_${index}`;
|
|
231
|
+
const lines = nugetWorkflowSteps(
|
|
232
|
+
{
|
|
233
|
+
...packages[0],
|
|
234
|
+
token_secrets: [...secrets, ...(packages[0].token_secrets ?? [])],
|
|
235
|
+
},
|
|
236
|
+
{ build: false, artifactGlob: guarded.glob },
|
|
237
|
+
)
|
|
238
|
+
.map((line) => prefix + line.slice(6))
|
|
239
|
+
.map((line) =>
|
|
240
|
+
line.replaceAll("prm_nuget_login", `prm_nuget_login_${index}`),
|
|
241
|
+
);
|
|
242
|
+
const loginIf = lines.findIndex((line) => /if: vars.NUGET_USER/.test(line));
|
|
243
|
+
if (gates) {
|
|
244
|
+
lines[loginIf] += ` && ${gates}`;
|
|
245
|
+
}
|
|
246
|
+
const publish = lines.findIndex((line) =>
|
|
247
|
+
/- name: Publish validated NuGet/.test(line),
|
|
248
|
+
);
|
|
249
|
+
lines.splice(
|
|
250
|
+
publish + 1,
|
|
251
|
+
0,
|
|
252
|
+
`${prefix} id: ${publishId}`,
|
|
253
|
+
...(gates ? [`${prefix} if: ${gates}`] : []),
|
|
254
|
+
);
|
|
255
|
+
const record = [
|
|
256
|
+
`${prefix}- name: ${name}`,
|
|
257
|
+
...(id ? [`${prefix} id: ${id}`] : []),
|
|
258
|
+
`${prefix} if: \${{ !cancelled()${condition ? ` && (${condition})` : ""} }}`,
|
|
259
|
+
`${prefix} env:`,
|
|
260
|
+
`${prefix} PRM_SHOULD_PUBLISH: ${guarded.guard ? `\${{ ${guarded.guard} }}` : "'true'"}`,
|
|
261
|
+
`${prefix} PRM_PUBLISH_OUTCOME: \${{ steps.${publishId}.outcome }}`,
|
|
262
|
+
`${prefix} run: |`,
|
|
263
|
+
`${prefix} if [ "$PRM_SHOULD_PUBLISH" != "true" ]; then`,
|
|
264
|
+
`${prefix} echo "published=skipped" >> "$GITHUB_OUTPUT"`,
|
|
265
|
+
`${prefix} exit 0`,
|
|
266
|
+
`${prefix} fi`,
|
|
267
|
+
`${prefix} if [ "$PRM_PUBLISH_OUTCOME" = "success" ]; then`,
|
|
268
|
+
`${prefix} echo "published=true" >> "$GITHUB_OUTPUT"`,
|
|
269
|
+
`${prefix} else`,
|
|
270
|
+
`${prefix} echo "published=failed" >> "$GITHUB_OUTPUT"`,
|
|
271
|
+
`${prefix} exit 1`,
|
|
272
|
+
`${prefix} fi`,
|
|
273
|
+
];
|
|
274
|
+
return [...lines, ...record];
|
|
275
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* global document */
|
|
2
|
+
/** Runs in the official NuGet Gallery page; never reads API keys or cookies. */
|
|
3
|
+
export default function nugetPage(expected, create = false) {
|
|
4
|
+
const username =
|
|
5
|
+
document.querySelector("#account-dropdown b span")?.textContent.trim() ??
|
|
6
|
+
null;
|
|
7
|
+
const model = globalThis.ko?.dataFor(document.body);
|
|
8
|
+
if (!model?.Policies) {
|
|
9
|
+
return { username, policies: [], ready: false };
|
|
10
|
+
}
|
|
11
|
+
const read = (object, name) =>
|
|
12
|
+
typeof object?.[name] === "function" ? object[name]() : object?.[name];
|
|
13
|
+
const policies = model
|
|
14
|
+
.Policies()
|
|
15
|
+
.filter((policy) => read(policy, "PublisherName") === "GitHubActions")
|
|
16
|
+
.map((policy) => ({
|
|
17
|
+
id: read(policy, "Key"),
|
|
18
|
+
package_owner: read(policy, "Owner"),
|
|
19
|
+
organization: read(policy.gitHub, "RepositoryOwner"),
|
|
20
|
+
repository: read(policy.gitHub, "Repository"),
|
|
21
|
+
owner_id: String(read(policy.gitHub, "RepositoryOwnerId") ?? ""),
|
|
22
|
+
repository_id: String(read(policy.gitHub, "RepositoryId") ?? ""),
|
|
23
|
+
workflow: read(policy.gitHub, "WorkflowFile"),
|
|
24
|
+
environment: read(policy.gitHub, "Environment") ?? "",
|
|
25
|
+
package_ids: read(policy, "PolicySubjects") ?? [],
|
|
26
|
+
operations: read(policy, "PolicyScopes") ?? [],
|
|
27
|
+
activation: !read(policy, "IsOwnerValid")
|
|
28
|
+
? "inactive"
|
|
29
|
+
: read(policy.gitHub, "IsPermanentlyEnabled")
|
|
30
|
+
? "active"
|
|
31
|
+
: read(policy.gitHub, "EnabledDaysLeft") > 0
|
|
32
|
+
? "temporarily-active"
|
|
33
|
+
: "inactive",
|
|
34
|
+
}));
|
|
35
|
+
const result = {
|
|
36
|
+
username,
|
|
37
|
+
policies,
|
|
38
|
+
ready: true,
|
|
39
|
+
pending: Boolean(read(model.NewPolicy(), "PendingCreateOrEdit")),
|
|
40
|
+
error: read(model, "Error") ?? null,
|
|
41
|
+
};
|
|
42
|
+
if (!create) {
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
if (username?.toLowerCase() !== expected.username.toLowerCase()) {
|
|
46
|
+
return { ...result, error: "wrong authenticated NuGet profile" };
|
|
47
|
+
}
|
|
48
|
+
// Never create another policy for a repository/workflow without explicit review.
|
|
49
|
+
if (
|
|
50
|
+
policies.some((policy) =>
|
|
51
|
+
["organization", "repository", "workflow", "environment"].every(
|
|
52
|
+
(field) =>
|
|
53
|
+
String(policy[field] ?? "").toLowerCase() ===
|
|
54
|
+
String(expected[field] ?? "").toLowerCase(),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
) {
|
|
58
|
+
return {
|
|
59
|
+
...result,
|
|
60
|
+
error:
|
|
61
|
+
"existing repository policy requires review; duplicate creation refused",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const draft = model.NewPolicy();
|
|
65
|
+
const owners = read(draft, "PackageOwners") ?? [];
|
|
66
|
+
const owner = owners.find(
|
|
67
|
+
(item) => item.Owner.toLowerCase() === expected.package_owner.toLowerCase(),
|
|
68
|
+
);
|
|
69
|
+
if (
|
|
70
|
+
!owner ||
|
|
71
|
+
expected.operations.length !== 1 ||
|
|
72
|
+
!(expected.operations[0] === "package:push"
|
|
73
|
+
? owner.CanPushNew
|
|
74
|
+
: owner.CanPushExisting)
|
|
75
|
+
) {
|
|
76
|
+
return {
|
|
77
|
+
...result,
|
|
78
|
+
error: "exact package owner or required operation is unavailable",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
draft.PackageOwner(owner);
|
|
82
|
+
draft.SelectedProvider("GitHubActions");
|
|
83
|
+
draft.PendingPolicyName(
|
|
84
|
+
`prm-${expected.organization}-${expected.repository}-${expected.workflow}`,
|
|
85
|
+
);
|
|
86
|
+
draft.gitHub.PendingRepositoryOwner(expected.organization);
|
|
87
|
+
draft.gitHub.PendingRepository(expected.repository);
|
|
88
|
+
draft.gitHub.PendingWorkflowFile(expected.workflow);
|
|
89
|
+
draft.gitHub.PendingEnvironment(expected.environment ?? "");
|
|
90
|
+
draft.PushScopeChecked(true);
|
|
91
|
+
draft.PushScope(expected.operations[0]);
|
|
92
|
+
draft.UnlistScopeChecked(false);
|
|
93
|
+
draft.StageScopeChecked(false);
|
|
94
|
+
draft.ScopeSubjectsInput(expected.package_ids.join("\n"));
|
|
95
|
+
if (!draft.Valid()) {
|
|
96
|
+
return { ...result, error: "NuGet rejected the exact policy fields" };
|
|
97
|
+
}
|
|
98
|
+
draft.CreateOrEdit();
|
|
99
|
+
return { ...result, submitted: true };
|
|
100
|
+
}
|