@brainervirus/workit-core 2.4.0 → 2.6.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/package.json +1 -1
- package/scripts/analyze-release-scope.ts +99 -8
- package/scripts/bundled-deps.json +133 -0
- package/scripts/publish-changed-packages.ts +28 -16
- package/scripts/rewrite-workspace-deps.ts +11 -5
- package/scripts/sync-release-manifests.ts +5 -1
- package/src/autonomy.ts +135 -0
- package/src/core/detect-hosts.ts +9 -3
- package/src/core/doctor.ts +61 -0
- package/src/core/external-action-effects.ts +4 -9
- package/src/core/host-install.ts +144 -1
- package/src/core/pr-create.ts +2 -71
- package/src/core/setup.ts +27 -15
- package/src/core/support-matrix.ts +4 -0
- package/src/core/uninstall.ts +107 -4
- package/src/forge/exec.ts +40 -0
- package/src/forge/github.ts +84 -1
- package/src/forge/gitlab.ts +102 -1
- package/src/forge/pr-body.ts +100 -0
- package/src/forge/pr-ops.ts +450 -0
- package/src/forge/report.ts +1 -1
- package/src/forge/types.ts +30 -0
- package/src/forge/verify.ts +398 -0
- package/src/git/ops.ts +626 -0
- package/src/git/rev.ts +24 -7
- package/src/hooks/handle.ts +15 -4
- package/src/ledger.ts +17 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// `workit verify-delivery` (design §2.1 S11): did the requested endpoint really
|
|
2
|
+
// happen? Each answer is an observation of the remote, never of local state:
|
|
3
|
+
// a local commit does not prove a push, a push does not prove a PR, and a
|
|
4
|
+
// "merged" claim needs the forge to say so.
|
|
5
|
+
//
|
|
6
|
+
// pushed the push remote's branch tip equals the expected SHA (ls-remote)
|
|
7
|
+
// pr the PR/MR exists, is open or merged, and its head is that SHA
|
|
8
|
+
// merged the forge reports the PR/MR merged; when workit merged it, the
|
|
9
|
+
// recorded merge commit is reachable from the base's remote tip
|
|
10
|
+
// released the tag exists on the remote (pointing at --sha when given),
|
|
11
|
+
// and the npm version is published (gitHead == the tag's commit)
|
|
12
|
+
//
|
|
13
|
+
// Read-only on the repository (fetches by object id move no ref). A delivered
|
|
14
|
+
// answer is appended to the ledger as an observed `delivery.verified` row.
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { findExecutable } from "./exec";
|
|
17
|
+
import {
|
|
18
|
+
aheadBehind,
|
|
19
|
+
currentBranch,
|
|
20
|
+
fetchRefs,
|
|
21
|
+
hasCommit,
|
|
22
|
+
pushRemoteName,
|
|
23
|
+
pushUrl,
|
|
24
|
+
redactRemote,
|
|
25
|
+
remoteNames,
|
|
26
|
+
remoteRefTip,
|
|
27
|
+
resolveRef,
|
|
28
|
+
} from "../git/rev";
|
|
29
|
+
import { appendObserved, readLedger, type LedgerActor } from "../ledger";
|
|
30
|
+
import { selectPr } from "./report";
|
|
31
|
+
import type { ResolvedForge } from "./resolve";
|
|
32
|
+
import { redactText } from "./redact";
|
|
33
|
+
import { failure, success, type ForgeResult } from "./types";
|
|
34
|
+
|
|
35
|
+
export type DeliveryEndpoint = "pushed" | "pr" | "merged" | "released";
|
|
36
|
+
|
|
37
|
+
export type Observation = {
|
|
38
|
+
kind:
|
|
39
|
+
| "remote_tip"
|
|
40
|
+
| "pr_state"
|
|
41
|
+
| "pr_head"
|
|
42
|
+
| "merge_on_base"
|
|
43
|
+
| "tag"
|
|
44
|
+
| "npm_version"
|
|
45
|
+
| "npm_git_head";
|
|
46
|
+
expected: string | null;
|
|
47
|
+
observed: string | null;
|
|
48
|
+
ok: boolean;
|
|
49
|
+
note?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type DeliveryReport = {
|
|
53
|
+
expect: DeliveryEndpoint;
|
|
54
|
+
branch: string | null;
|
|
55
|
+
sha: string | null;
|
|
56
|
+
pr: number | null;
|
|
57
|
+
delivered: boolean;
|
|
58
|
+
observations: Observation[];
|
|
59
|
+
recorded?: { id: string } | { error: string };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Runs `npm view …` (injected in tests). */
|
|
63
|
+
export type NpmRunner = (args: readonly string[]) => {
|
|
64
|
+
status: number | null;
|
|
65
|
+
stdout: string;
|
|
66
|
+
stderr: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// npm package names (URL-safe, optional @scope/) and semver.
|
|
70
|
+
// Both are checked before npm runs, so no argument can carry shell syntax.
|
|
71
|
+
// Upper case is accepted: legacy packages (published before the lower-case
|
|
72
|
+
// rule) are still installable, and this only looks them up.
|
|
73
|
+
const NPM_NAME = /^(?:@[A-Za-z0-9~][A-Za-z0-9-._~]*\/)?[A-Za-z0-9~][A-Za-z0-9-._~]*$/u;
|
|
74
|
+
const SEMVER =
|
|
75
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
|
|
76
|
+
const SAFE_ARG = /^[0-9A-Za-z@/._~+-]+$/u;
|
|
77
|
+
|
|
78
|
+
export const systemNpm: NpmRunner = (args) => {
|
|
79
|
+
// Windows ships npm as npm.cmd, which only runs through cmd.exe. Every
|
|
80
|
+
// argument is a fixed word or a validated name@semver (SAFE_ARG), so the
|
|
81
|
+
// shell has nothing to interpret; anything else is refused before spawning.
|
|
82
|
+
if (!args.every((arg) => SAFE_ARG.test(arg)))
|
|
83
|
+
return { status: 2, stdout: "", stderr: "refused: unsafe npm argument" };
|
|
84
|
+
const options = {
|
|
85
|
+
encoding: "utf8" as const,
|
|
86
|
+
timeout: 30_000,
|
|
87
|
+
killSignal: "SIGKILL" as const,
|
|
88
|
+
env: { ...process.env, NO_UPDATE_NOTIFIER: "1", npm_config_update_notifier: "false" },
|
|
89
|
+
windowsHide: true,
|
|
90
|
+
stdio: ["ignore", "pipe", "pipe"] as ["ignore", "pipe", "pipe"],
|
|
91
|
+
};
|
|
92
|
+
// On Windows one pre-validated command string goes to cmd (no args array
|
|
93
|
+
// with shell: true, which is deprecated as DEP0190).
|
|
94
|
+
const found = process.platform === "win32" ? findExecutable("npm.cmd", process.env) : null;
|
|
95
|
+
// A path cmd would expand (%VAR%, !VAR!, ^, ") falls back to PATH lookup.
|
|
96
|
+
const npmCmd = found && !/["%!^]/u.test(found) ? `"${found}"` : "npm.cmd";
|
|
97
|
+
const result =
|
|
98
|
+
process.platform === "win32"
|
|
99
|
+
? spawnSync(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true })
|
|
100
|
+
: spawnSync("npm", [...args], options);
|
|
101
|
+
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const short = (sha: string | null): string => (sha ? sha.slice(0, 12) : "(absent)");
|
|
105
|
+
|
|
106
|
+
function finish(
|
|
107
|
+
cwd: string,
|
|
108
|
+
report: Omit<DeliveryReport, "delivered">,
|
|
109
|
+
actor: LedgerActor,
|
|
110
|
+
): DeliveryReport {
|
|
111
|
+
const delivered =
|
|
112
|
+
report.observations.length > 0 && report.observations.every((entry) => entry.ok);
|
|
113
|
+
const out: DeliveryReport = { ...report, delivered };
|
|
114
|
+
if (delivered) {
|
|
115
|
+
const row = appendObserved(cwd, {
|
|
116
|
+
type: "delivery.verified",
|
|
117
|
+
actor,
|
|
118
|
+
branch: report.branch,
|
|
119
|
+
head: report.sha,
|
|
120
|
+
...(report.pr === null ? {} : { pr: report.pr }),
|
|
121
|
+
expect: report.expect,
|
|
122
|
+
observations: report.observations.map((entry) => ({
|
|
123
|
+
kind: entry.kind,
|
|
124
|
+
observed: entry.observed,
|
|
125
|
+
})),
|
|
126
|
+
});
|
|
127
|
+
out.recorded = row.ok ? { id: String(row.value.id) } : { error: row.error };
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The pushed-branch observation: remote tip vs the expected (default: local) SHA. */
|
|
133
|
+
export function verifyPushed(
|
|
134
|
+
cwd: string,
|
|
135
|
+
input: { branch?: string | null; sha?: string | null; actor: LedgerActor },
|
|
136
|
+
): ForgeResult<DeliveryReport> {
|
|
137
|
+
const branch = input.branch ?? currentBranch(cwd);
|
|
138
|
+
if (!branch) return failure("invalid_input", "HEAD is detached; pass --branch <name>");
|
|
139
|
+
const sha = input.sha ?? resolveRef(cwd, `refs/heads/${branch}`);
|
|
140
|
+
if (!sha) return failure("not_found", `branch ${branch} has no local commit; pass --sha <sha>`);
|
|
141
|
+
const remote = pushRemoteName(cwd, branch);
|
|
142
|
+
if (!remote) return failure("not_found", "no push remote is configured for this branch");
|
|
143
|
+
const url = pushUrl(cwd, remote);
|
|
144
|
+
if (!url) return failure("blocked", `remote "${remote}" has no single push URL`);
|
|
145
|
+
const tip = remoteRefTip(cwd, url, `refs/heads/${branch}`);
|
|
146
|
+
if (!tip.ok) return failure(tip.code, tip.error);
|
|
147
|
+
const observation: Observation = {
|
|
148
|
+
kind: "remote_tip",
|
|
149
|
+
expected: sha,
|
|
150
|
+
observed: tip.sha,
|
|
151
|
+
ok: tip.sha !== null && tip.sha.startsWith(sha),
|
|
152
|
+
};
|
|
153
|
+
if (!observation.ok) {
|
|
154
|
+
const counts = tip.sha && hasCommit(cwd, tip.sha) ? aheadBehind(cwd, tip.sha, sha) : null;
|
|
155
|
+
observation.note =
|
|
156
|
+
tip.sha === null
|
|
157
|
+
? `${branch} does not exist on ${redactRemote(url)}: nothing was pushed`
|
|
158
|
+
: counts
|
|
159
|
+
? `${counts.ahead} local commit(s) not on ${remote}/${branch}${counts.behind ? `, ${counts.behind} remote commit(s) not local` : ""}`
|
|
160
|
+
: `${remote}/${branch} is at ${short(tip.sha)}, expected ${short(sha)}`;
|
|
161
|
+
}
|
|
162
|
+
return success(
|
|
163
|
+
finish(
|
|
164
|
+
cwd,
|
|
165
|
+
{ expect: "pushed", branch, sha, pr: null, observations: [observation] },
|
|
166
|
+
input.actor,
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The PR/MR exists, is open or merged, and its head is the expected SHA. */
|
|
172
|
+
export function verifyPr(
|
|
173
|
+
cwd: string,
|
|
174
|
+
resolved: ResolvedForge,
|
|
175
|
+
input: { pr?: number | null; branch?: string | null; sha?: string | null; actor: LedgerActor },
|
|
176
|
+
): ForgeResult<DeliveryReport> {
|
|
177
|
+
const number = selectPr(cwd, resolved, { pr: input.pr ?? null, branch: input.branch ?? null });
|
|
178
|
+
if (!number.ok) {
|
|
179
|
+
if (number.code !== "not_found") return number;
|
|
180
|
+
const branch = input.branch ?? currentBranch(cwd);
|
|
181
|
+
return success(
|
|
182
|
+
finish(
|
|
183
|
+
cwd,
|
|
184
|
+
{
|
|
185
|
+
expect: "pr",
|
|
186
|
+
branch,
|
|
187
|
+
sha: input.sha ?? null,
|
|
188
|
+
pr: null,
|
|
189
|
+
observations: [
|
|
190
|
+
{ kind: "pr_state", expected: "open", observed: null, ok: false, note: number.error },
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
input.actor,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const status = resolved.forge.prStatus(number.data);
|
|
198
|
+
if (!status.ok) return status;
|
|
199
|
+
const pr = status.data;
|
|
200
|
+
const sha = input.sha ?? resolveRef(cwd, `refs/heads/${pr.head.branch}`) ?? pr.head.sha;
|
|
201
|
+
const observations: Observation[] = [
|
|
202
|
+
{ kind: "pr_state", expected: "open|merged", observed: pr.state, ok: pr.state !== "closed" },
|
|
203
|
+
{
|
|
204
|
+
kind: "pr_head",
|
|
205
|
+
expected: sha,
|
|
206
|
+
observed: pr.head.sha,
|
|
207
|
+
ok: pr.head.sha.startsWith(sha),
|
|
208
|
+
...(pr.head.sha.startsWith(sha)
|
|
209
|
+
? {}
|
|
210
|
+
: { note: `the PR head is ${short(pr.head.sha)}; push ${short(sha)} (workit git push)` }),
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
return success(
|
|
214
|
+
finish(
|
|
215
|
+
cwd,
|
|
216
|
+
{ expect: "pr", branch: pr.head.branch, sha, pr: pr.number, observations },
|
|
217
|
+
input.actor,
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The forge reports the PR/MR merged, and a recorded merge commit is on the base. */
|
|
223
|
+
export function verifyMerged(
|
|
224
|
+
cwd: string,
|
|
225
|
+
resolved: ResolvedForge,
|
|
226
|
+
input: { pr?: number | null; branch?: string | null; actor: LedgerActor },
|
|
227
|
+
): ForgeResult<DeliveryReport> {
|
|
228
|
+
const number = selectPr(cwd, resolved, { pr: input.pr ?? null, branch: input.branch ?? null });
|
|
229
|
+
if (!number.ok) return number;
|
|
230
|
+
const status = resolved.forge.prStatus(number.data);
|
|
231
|
+
if (!status.ok) return status;
|
|
232
|
+
const pr = status.data;
|
|
233
|
+
const observations: Observation[] = [
|
|
234
|
+
{ kind: "pr_state", expected: "merged", observed: pr.state, ok: pr.state === "merged" },
|
|
235
|
+
];
|
|
236
|
+
if (pr.state === "merged") {
|
|
237
|
+
const ledger = readLedger(cwd);
|
|
238
|
+
const row = ledger.ok
|
|
239
|
+
? ledger.value.rows.findLast(
|
|
240
|
+
(entry) =>
|
|
241
|
+
entry.type === "pr.merged" && entry.pr === pr.number && entry.observer === "workit_cli",
|
|
242
|
+
)
|
|
243
|
+
: undefined;
|
|
244
|
+
const mergeSha = typeof row?.mergeSha === "string" ? row.mergeSha : null;
|
|
245
|
+
if (mergeSha) {
|
|
246
|
+
const tip = remoteRefTip(cwd, resolved.baseRemote, `refs/heads/${pr.base}`);
|
|
247
|
+
if (!tip.ok) return failure(tip.code, tip.error);
|
|
248
|
+
let onBase = false;
|
|
249
|
+
let note: string | undefined;
|
|
250
|
+
if (tip.sha) {
|
|
251
|
+
for (const sha of [mergeSha, tip.sha])
|
|
252
|
+
if (!hasCommit(cwd, sha)) fetchRefs(cwd, resolved.baseRemote, [sha]);
|
|
253
|
+
const counts =
|
|
254
|
+
hasCommit(cwd, mergeSha) && hasCommit(cwd, tip.sha)
|
|
255
|
+
? aheadBehind(cwd, tip.sha, mergeSha)
|
|
256
|
+
: null;
|
|
257
|
+
onBase = counts !== null && counts.ahead === 0;
|
|
258
|
+
if (!counts) note = "the merge commit or the base tip could not be fetched";
|
|
259
|
+
} else note = `${pr.base} does not exist on the remote`;
|
|
260
|
+
observations.push({
|
|
261
|
+
kind: "merge_on_base",
|
|
262
|
+
expected: mergeSha,
|
|
263
|
+
observed: tip.sha,
|
|
264
|
+
ok: onBase,
|
|
265
|
+
...(note
|
|
266
|
+
? { note }
|
|
267
|
+
: onBase
|
|
268
|
+
? {}
|
|
269
|
+
: { note: `${short(mergeSha)} is not reachable from ${pr.base}` }),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return success(
|
|
274
|
+
finish(
|
|
275
|
+
cwd,
|
|
276
|
+
{ expect: "merged", branch: pr.head.branch, sha: pr.head.sha, pr: pr.number, observations },
|
|
277
|
+
input.actor,
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** `@scope/name@1.2.3` → name + version (version may be absent). */
|
|
283
|
+
/**
|
|
284
|
+
* `@scope/name@1.2.3` → name + version (version may be absent). Null unless
|
|
285
|
+
* the name follows npm's naming rules and the version is exact semver.
|
|
286
|
+
*/
|
|
287
|
+
export function parsePackageSpec(spec: string): { name: string; version: string | null } | null {
|
|
288
|
+
const value = spec.trim();
|
|
289
|
+
const at = value.lastIndexOf("@");
|
|
290
|
+
const name = at > 0 ? value.slice(0, at) : value;
|
|
291
|
+
const version = at > 0 ? value.slice(at + 1) : null;
|
|
292
|
+
if (!name || name.length > 214 || !NPM_NAME.test(name)) return null;
|
|
293
|
+
if (version !== null && !SEMVER.test(version)) return null;
|
|
294
|
+
return { name, version };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
type NpmView = { version?: unknown; gitHead?: unknown };
|
|
298
|
+
|
|
299
|
+
const npmView = (stdout: string): NpmView | null => {
|
|
300
|
+
try {
|
|
301
|
+
const parsed = JSON.parse(stdout) as unknown;
|
|
302
|
+
const value = Array.isArray(parsed) ? parsed.at(-1) : parsed;
|
|
303
|
+
return value && typeof value === "object" ? (value as NpmView) : null;
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export function verifyReleased(
|
|
310
|
+
cwd: string,
|
|
311
|
+
input: { tag?: string | null; pkg?: string | null; sha?: string | null; actor: LedgerActor },
|
|
312
|
+
npm: NpmRunner = systemNpm,
|
|
313
|
+
): ForgeResult<DeliveryReport> {
|
|
314
|
+
if (!input.tag && !input.pkg)
|
|
315
|
+
return failure("invalid_input", "released needs --tag <tag> and/or --package <name[@version]>");
|
|
316
|
+
if (input.tag && (input.tag.startsWith("-") || !/^[0-9A-Za-z._/+-]+$/u.test(input.tag)))
|
|
317
|
+
return failure("invalid_input", "--tag must be a plain tag name");
|
|
318
|
+
const observations: Observation[] = [];
|
|
319
|
+
let tagSha: string | null = null;
|
|
320
|
+
if (input.tag) {
|
|
321
|
+
const remote = remoteNames(cwd).includes("origin") ? "origin" : pushRemoteName(cwd);
|
|
322
|
+
if (!remote) return failure("not_found", "no remote is configured");
|
|
323
|
+
const tip = remoteRefTip(cwd, remote, `refs/tags/${input.tag}`);
|
|
324
|
+
if (!tip.ok) return failure(tip.code, tip.error);
|
|
325
|
+
tagSha = tip.sha;
|
|
326
|
+
const expected = input.sha ?? null;
|
|
327
|
+
observations.push({
|
|
328
|
+
kind: "tag",
|
|
329
|
+
expected: expected ?? input.tag,
|
|
330
|
+
observed: tip.sha,
|
|
331
|
+
ok: tip.sha !== null && (expected === null || tip.sha.startsWith(expected)),
|
|
332
|
+
...(tip.sha === null ? { note: `tag ${input.tag} is not on ${remote}` } : {}),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (input.pkg) {
|
|
336
|
+
const spec = parsePackageSpec(input.pkg);
|
|
337
|
+
if (!spec)
|
|
338
|
+
return failure(
|
|
339
|
+
"invalid_input",
|
|
340
|
+
"--package must be an npm package name, optionally @<semver> (e.g. @scope/pkg@1.2.3)",
|
|
341
|
+
);
|
|
342
|
+
const version = spec.version ?? (input.tag ? input.tag.replace(/^v(?=\d)/u, "") : null);
|
|
343
|
+
if (!version)
|
|
344
|
+
return failure("invalid_input", "--package needs a version (<name>@<version>) or --tag");
|
|
345
|
+
if (!SEMVER.test(version))
|
|
346
|
+
return failure(
|
|
347
|
+
"invalid_input",
|
|
348
|
+
`${version} (from --tag) is not a semver version; pass <name>@<version>`,
|
|
349
|
+
);
|
|
350
|
+
const viewed = npm(["view", `${spec.name}@${version}`, "version", "gitHead", "--json"]);
|
|
351
|
+
if (
|
|
352
|
+
viewed.status !== 0 &&
|
|
353
|
+
!/E404|404 Not Found|is not in this registry|No match found/iu.test(viewed.stderr)
|
|
354
|
+
)
|
|
355
|
+
return failure(
|
|
356
|
+
"unavailable",
|
|
357
|
+
`npm view ${spec.name}@${version} failed: ${redactText(viewed.stderr).trim().split("\n")[0]?.slice(0, 200) ?? ""}`,
|
|
358
|
+
);
|
|
359
|
+
const observed = viewed.status === 0 ? npmView(viewed.stdout) : null;
|
|
360
|
+
const observedVersion = typeof observed?.version === "string" ? observed.version : null;
|
|
361
|
+
observations.push({
|
|
362
|
+
kind: "npm_version",
|
|
363
|
+
expected: `${spec.name}@${version}`,
|
|
364
|
+
observed: observedVersion ? `${spec.name}@${observedVersion}` : null,
|
|
365
|
+
ok: observedVersion === version,
|
|
366
|
+
...(observedVersion === version ? {} : { note: `${spec.name}@${version} is not published` }),
|
|
367
|
+
});
|
|
368
|
+
// A commit expectation (the tag's commit, else --sha) must be proven by
|
|
369
|
+
// the published gitHead; a package without one is unverifiable, not ok.
|
|
370
|
+
const gitHead = typeof observed?.gitHead === "string" ? observed.gitHead : null;
|
|
371
|
+
const commit = tagSha ?? input.sha ?? null;
|
|
372
|
+
if (commit && observedVersion === version)
|
|
373
|
+
observations.push({
|
|
374
|
+
kind: "npm_git_head",
|
|
375
|
+
expected: commit,
|
|
376
|
+
observed: gitHead,
|
|
377
|
+
ok: gitHead !== null && gitHead.startsWith(commit),
|
|
378
|
+
...(gitHead === null
|
|
379
|
+
? { note: `unverified: ${spec.name}@${version} was published without a gitHead` }
|
|
380
|
+
: gitHead.startsWith(commit)
|
|
381
|
+
? {}
|
|
382
|
+
: { note: `${spec.name}@${version} was published from ${short(gitHead)}` }),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return success(
|
|
386
|
+
finish(
|
|
387
|
+
cwd,
|
|
388
|
+
{
|
|
389
|
+
expect: "released",
|
|
390
|
+
branch: null,
|
|
391
|
+
sha: tagSha ?? input.sha ?? null,
|
|
392
|
+
pr: null,
|
|
393
|
+
observations,
|
|
394
|
+
},
|
|
395
|
+
input.actor,
|
|
396
|
+
),
|
|
397
|
+
);
|
|
398
|
+
}
|