@workit-dev/core 0.0.1 → 1.0.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/LICENSE +21 -0
- package/README.md +22 -43
- package/package.json +56 -6
- package/schemas/config.json +74 -0
- package/schemas/vcs.json +79 -0
- package/schemas/workspaces.json +1029 -0
- package/schemas/youtrack.json +84 -0
- package/scripts/analyze-release-scope.ts +215 -0
- package/scripts/bundled-deps.json +131 -0
- package/scripts/fixtures/sample-plan.md +15 -0
- package/scripts/fixtures/sample-sdd/progress.md +2 -0
- package/scripts/install-codex-plugin.sh +28 -0
- package/scripts/install-cursor-plugin.sh +146 -0
- package/scripts/install-opencode-plugin.sh +79 -0
- package/scripts/install-pi-package.sh +56 -0
- package/scripts/lib/config-dir.sh +28 -0
- package/scripts/publish-changed-packages.ts +395 -0
- package/scripts/rewrite-workspace-deps.ts +53 -0
- package/scripts/run-cursor-mcp.sh +6 -0
- package/scripts/sync-release-manifests.ts +82 -0
- package/scripts/sync-runtime.sh +142 -0
- package/scripts/validate-cursor-marketplace.ts +171 -0
- package/scripts/verify-bundle-sources.ts +134 -0
- package/scripts/write-schemas.ts +16 -0
- package/skills/workit-architecture/SKILL.md +72 -0
- package/skills/workit-architecture/agents/openai.yaml +3 -0
- package/skills/workit-architecture/references/instruction-files.md +52 -0
- package/skills/workit-architecture/references/vocabulary.md +53 -0
- package/skills/workit-bdd/SKILL.md +53 -0
- package/skills/workit-continue/SKILL.md +54 -0
- package/skills/workit-debug/SKILL.md +45 -0
- package/skills/workit-deslop/SKILL.md +58 -0
- package/skills/workit-fanout/SKILL.md +71 -0
- package/skills/workit-fanout/references/brief.md +141 -0
- package/skills/workit-fanout/references/orchestration.md +169 -0
- package/skills/workit-implement/SKILL.md +71 -0
- package/skills/workit-retro/SKILL.md +72 -0
- package/skills/workit-retro/agents/openai.yaml +3 -0
- package/skills/workit-retro/references/sources.md +35 -0
- package/skills/workit-retro/references/steering.md +39 -0
- package/skills/workit-review/SKILL.md +66 -0
- package/skills/workit-review/references/impact.md +24 -0
- package/skills/workit-shape/SKILL.md +71 -0
- package/skills/workit-shape/references/diagrams.md +17 -0
- package/skills/workit-shape/references/knowledge.md +61 -0
- package/skills/workit-shape/references/mockups.md +15 -0
- package/skills/workit-shape/references/slicing.md +44 -0
- package/skills/workit-ship/SKILL.md +71 -0
- package/skills/workit-ship/references/changelog.md +44 -0
- package/skills/workit-ship/references/pr-body.md +32 -0
- package/skills/workit-ship/references/release.md +39 -0
- package/skills/workit-test-audit/SKILL.md +54 -0
- package/skills/workit-verify-app/SKILL.md +68 -0
- package/skills/workit-verify-app/references/template.md +51 -0
- package/src/autonomy.ts +910 -0
- package/src/check-config.ts +613 -0
- package/src/checks.ts +534 -0
- package/src/ci-waiter.ts +159 -0
- package/src/core/boundary.ts +55 -0
- package/src/core/branch-policy.ts +112 -0
- package/src/core/branch.ts +1192 -0
- package/src/core/changelog-gate.ts +349 -0
- package/src/core/changelog-merge.ts +214 -0
- package/src/core/changelog.ts +392 -0
- package/src/core/cleanup.ts +638 -0
- package/src/core/cli-locate.ts +173 -0
- package/src/core/commit-flavors.ts +65 -0
- package/src/core/config-schemas.ts +653 -0
- package/src/core/config.ts +351 -0
- package/src/core/context-read.ts +144 -0
- package/src/core/docs-gate.ts +69 -0
- package/src/core/finding-evidence.ts +89 -0
- package/src/core/finding-queue.ts +36 -0
- package/src/core/git.ts +73 -0
- package/src/core/gitignore.ts +52 -0
- package/src/core/heavy-eta.ts +170 -0
- package/src/core/heavy-lease.ts +290 -0
- package/src/core/heavy-rules.ts +373 -0
- package/src/core/heavy-scope.ts +131 -0
- package/src/core/heavy-words.ts +83 -0
- package/src/core/heavy.ts +535 -0
- package/src/core/hygiene.ts +149 -0
- package/src/core/init.ts +218 -0
- package/src/core/live-session.ts +333 -0
- package/src/core/logger.ts +310 -0
- package/src/core/methods.ts +185 -0
- package/src/core/model-fallback.ts +208 -0
- package/src/core/node-resolve.mjs +249 -0
- package/src/core/operation-input.ts +632 -0
- package/src/core/package-root.ts +28 -0
- package/src/core/policy/attributes.ts +97 -0
- package/src/core/policy/bookkeeping.ts +119 -0
- package/src/core/policy/carry.ts +122 -0
- package/src/core/policy/check-evidence.ts +115 -0
- package/src/core/policy/current-judgment.ts +20 -0
- package/src/core/policy/derive.ts +387 -0
- package/src/core/policy/judgment.ts +246 -0
- package/src/core/policy/lane.ts +562 -0
- package/src/core/policy/pr-lane.ts +32 -0
- package/src/core/policy/substance.ts +365 -0
- package/src/core/policy-resolver.ts +94 -0
- package/src/core/pr-create.ts +448 -0
- package/src/core/pr-template.ts +162 -0
- package/src/core/release-notes.ts +197 -0
- package/src/core/release-tracks.ts +779 -0
- package/src/core/remotes.ts +436 -0
- package/src/core/repo-context.ts +542 -0
- package/src/core/repo-paths.ts +29 -0
- package/src/core/resources.ts +1299 -0
- package/src/core/role-models.ts +603 -0
- package/src/core/route-intent.ts +305 -0
- package/src/core/rules.ts +65 -0
- package/src/core/runtime-identity.ts +63 -0
- package/src/core/safe-write.ts +87 -0
- package/src/core/scope-glob.ts +56 -0
- package/src/core/scripts.ts +6 -0
- package/src/core/skill-manifests.ts +173 -0
- package/src/core/store-lock.ts +374 -0
- package/src/core/support-matrix.ts +24 -0
- package/src/core/task-context.ts +247 -0
- package/src/core/task-contract.ts +1279 -0
- package/src/core/task-delivery.ts +109 -0
- package/src/core/task-engine.ts +2248 -0
- package/src/core/task-evaluation.ts +1147 -0
- package/src/core/task-store.ts +2649 -0
- package/src/core/task-view.ts +180 -0
- package/src/core/templates.ts +22 -0
- package/src/core/tracker-issues.ts +174 -0
- package/src/core/vcs-config.ts +540 -0
- package/src/core/workers.ts +62 -0
- package/src/core/workit-paths.mjs +242 -0
- package/src/core/workspace-branch-policy.ts +123 -0
- package/src/core/workspaces.ts +1745 -0
- package/src/core/youtrack-backend.ts +288 -0
- package/src/core/youtrack-tools.ts +260 -0
- package/src/core/youtrack-writes.ts +73 -0
- package/src/core/youtrack-ytrack.ts +217 -0
- package/src/core/youtrack.ts +454 -0
- package/src/core/ytrack-skill.ts +297 -0
- package/src/core.ts +171 -0
- package/src/deslop/lex.ts +271 -0
- package/src/deslop/scan.ts +232 -0
- package/src/duration.ts +27 -0
- package/src/fanout-brief.ts +486 -0
- package/src/fanout-check.ts +455 -0
- package/src/fanout-landed.ts +370 -0
- package/src/fanout-stack.ts +108 -0
- package/src/fanout-status.ts +362 -0
- package/src/fanout-worktree.ts +893 -0
- package/src/fanout.ts +1473 -0
- package/src/forge/exec.ts +385 -0
- package/src/forge/github.ts +831 -0
- package/src/forge/gitlab.ts +906 -0
- package/src/forge/issue-forge.ts +209 -0
- package/src/forge/issue-link.ts +468 -0
- package/src/forge/issue-ops.ts +314 -0
- package/src/forge/merge-gate-base-red.ts +72 -0
- package/src/forge/pr-body.ts +100 -0
- package/src/forge/pr-edit.ts +292 -0
- package/src/forge/pr-ops.ts +1109 -0
- package/src/forge/pr-status.ts +10 -0
- package/src/forge/redact.ts +195 -0
- package/src/forge/report.ts +994 -0
- package/src/forge/reruns.ts +132 -0
- package/src/forge/resolve.ts +509 -0
- package/src/forge/types.ts +242 -0
- package/src/forge/verify.ts +398 -0
- package/src/git/default-base.ts +130 -0
- package/src/git/env-redirect.ts +28 -0
- package/src/git/facts.ts +122 -0
- package/src/git/ops.ts +1823 -0
- package/src/git/refs-read.ts +332 -0
- package/src/git/release.ts +1020 -0
- package/src/git/rev.ts +1175 -0
- package/src/git/session-hook.ts +597 -0
- package/src/git/stage.ts +63 -0
- package/src/hooks/agent-model.ts +149 -0
- package/src/hooks/budget-signal.ts +284 -0
- package/src/hooks/context.ts +461 -0
- package/src/hooks/cwd-normalize.ts +79 -0
- package/src/hooks/deadline.ts +228 -0
- package/src/hooks/delivery-verbs.ts +182 -0
- package/src/hooks/descriptor.ts +124 -0
- package/src/hooks/handle.ts +433 -0
- package/src/hooks/heavy-gate.ts +99 -0
- package/src/hooks/hook-runs.ts +130 -0
- package/src/hooks/hosts/claude-code.ts +380 -0
- package/src/hooks/hosts/codex.ts +372 -0
- package/src/hooks/hosts/cursor.ts +419 -0
- package/src/hooks/hosts/fields.ts +98 -0
- package/src/hooks/hosts/opencode.ts +79 -0
- package/src/hooks/hosts/pi.ts +78 -0
- package/src/hooks/index.ts +60 -0
- package/src/hooks/model-limit.ts +378 -0
- package/src/hooks/model-switch.ts +35 -0
- package/src/hooks/policy.ts +29 -0
- package/src/hooks/process.ts +48 -0
- package/src/hooks/protocol.ts +135 -0
- package/src/hooks/raw-git.ts +2289 -0
- package/src/hooks/repeat-failure.ts +115 -0
- package/src/hooks/run.ts +54 -0
- package/src/hooks/session-marker.ts +151 -0
- package/src/hooks/shell-words.ts +532 -0
- package/src/hooks/skill-nudge.ts +295 -0
- package/src/hooks/spawn-gate.ts +72 -0
- package/src/hooks/standing-signal.ts +99 -0
- package/src/hooks/standing.ts +225 -0
- package/src/hooks/steering.ts +74 -0
- package/src/hooks/stop.ts +408 -0
- package/src/hooks/worktree-confine.ts +415 -0
- package/src/hooks/write-gate.ts +1174 -0
- package/src/host-session.ts +459 -0
- package/src/knowledge.ts +368 -0
- package/src/ledger.ts +2913 -0
- package/src/stack.ts +2570 -0
- package/src/store/event-log.ts +217 -0
- package/src/store/notes.ts +29 -0
- package/src/store/patch.ts +135 -0
- package/src/store/paths.ts +368 -0
- package/src/store/reduce.ts +203 -0
- package/src/test-audit-mocks.ts +309 -0
- package/src/verdict-cover.ts +151 -0
- package/src/worktree-deps.ts +142 -0
- package/templates/headers.md +3 -0
- package/templates/hygiene/.editorconfig +8 -0
- package/templates/hygiene/.gitattributes +3 -0
- package/templates/hygiene/CHANGELOG.md +14 -0
- package/templates/hygiene/CONTRIBUTING.md +3 -0
- package/templates/hygiene/LICENSE +21 -0
- package/templates/hygiene/README.md +3 -0
- package/templates/issue-update.md +5 -0
- package/templates/plan-template.md +26 -0
- package/templates/spec-template.md +64 -0
- package/templates/workit-contract.md +13 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Forge-neutral shapes for the GitHub and GitLab adapters (design §2.0, S10).
|
|
2
|
+
// Plain TS: the CLI verbs load this on their path.
|
|
3
|
+
import type { ForgeKind } from "../git/rev";
|
|
4
|
+
|
|
5
|
+
export type { ForgeKind };
|
|
6
|
+
|
|
7
|
+
export type ForgeErrorCode =
|
|
8
|
+
| "unavailable"
|
|
9
|
+
| "not_found"
|
|
10
|
+
| "failed"
|
|
11
|
+
| "blocked"
|
|
12
|
+
| "busy"
|
|
13
|
+
| "invalid_input";
|
|
14
|
+
|
|
15
|
+
export type ForgeResult<T> =
|
|
16
|
+
| { ok: true; data: T }
|
|
17
|
+
| { ok: false; code: ForgeErrorCode; error: string; unblock?: string };
|
|
18
|
+
|
|
19
|
+
export const success = <T>(data: T): ForgeResult<T> => ({ ok: true, data });
|
|
20
|
+
|
|
21
|
+
export const failure = <T = never>(
|
|
22
|
+
code: ForgeErrorCode,
|
|
23
|
+
error: string,
|
|
24
|
+
unblock?: string,
|
|
25
|
+
): ForgeResult<T> => ({ ok: false, code, error, ...(unblock ? { unblock } : {}) });
|
|
26
|
+
|
|
27
|
+
export type PrState = "open" | "closed" | "merged";
|
|
28
|
+
|
|
29
|
+
export type PrRef = {
|
|
30
|
+
number: number;
|
|
31
|
+
url: string;
|
|
32
|
+
state: PrState;
|
|
33
|
+
headBranch: string;
|
|
34
|
+
headSha: string | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** How a branch-name lookup proves a PR is ours (M5): head owner/project or head sha. */
|
|
38
|
+
export type PrMatch = {
|
|
39
|
+
/** GitHub: the head repository owner (the push repo's owner). */
|
|
40
|
+
owner: string | null;
|
|
41
|
+
/** GitLab: the source project id (the push project). */
|
|
42
|
+
projectId: number | null;
|
|
43
|
+
/** A PR whose head is exactly this commit is ours whatever its owner. */
|
|
44
|
+
sha: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Only a PR into this base: one head may have PRs into several bases (a
|
|
47
|
+
* git-flow release into main and its back-merge into develop).
|
|
48
|
+
*/
|
|
49
|
+
base?: string | null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** One CI check (GitHub check run / commit status, GitLab job or bridge). */
|
|
53
|
+
export type ForgeCheck = {
|
|
54
|
+
/** `<workflow> / <job>` on GitHub, `<stage> / <job>` on GitLab, or the status context. */
|
|
55
|
+
name: string;
|
|
56
|
+
/** The raw context/job name branch protection refers to. */
|
|
57
|
+
context: string;
|
|
58
|
+
state: "failing" | "pending" | "passing" | "skipped";
|
|
59
|
+
/** The forge's own verdict, lower-cased (failure, timed_out, failed, canceled…). */
|
|
60
|
+
conclusion: string | null;
|
|
61
|
+
url: string | null;
|
|
62
|
+
/** GitHub workflow run id / GitLab pipeline id. */
|
|
63
|
+
runId: number | null;
|
|
64
|
+
/** GitHub Actions job id / GitLab job id; null for external statuses and bridges. */
|
|
65
|
+
jobId: number | null;
|
|
66
|
+
/** GitLab: the project that owns the pipeline (forks run elsewhere). */
|
|
67
|
+
scope: string | null;
|
|
68
|
+
/** Required by branch protection (GitHub isRequired); null when the forge cannot say. */
|
|
69
|
+
required: boolean | null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type ForgeThread = {
|
|
73
|
+
id: string;
|
|
74
|
+
path: string | null;
|
|
75
|
+
line: number | null;
|
|
76
|
+
author: string | null;
|
|
77
|
+
isBot: boolean;
|
|
78
|
+
outdated: boolean;
|
|
79
|
+
url: string | null;
|
|
80
|
+
/** First comment, redacted and cut to 300 chars. Untrusted data. */
|
|
81
|
+
body: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type ForgePrStatus = {
|
|
85
|
+
number: number;
|
|
86
|
+
url: string;
|
|
87
|
+
state: PrState;
|
|
88
|
+
draft: boolean;
|
|
89
|
+
base: string;
|
|
90
|
+
/** The current tip of the base branch, from the API. */
|
|
91
|
+
baseSha: string | null;
|
|
92
|
+
head: { branch: string; sha: string };
|
|
93
|
+
mergeable: "yes" | "no" | "unknown";
|
|
94
|
+
conflicts: boolean;
|
|
95
|
+
/** Branch protection requires the head to be up to date and it is not. */
|
|
96
|
+
rebaseRequired: boolean;
|
|
97
|
+
/** approved | changes_requested | review_required | null. */
|
|
98
|
+
reviewDecision: string | null;
|
|
99
|
+
/** GitHub mergeStateStatus / GitLab detailed_merge_status, lower-cased. */
|
|
100
|
+
mergeState: string | null;
|
|
101
|
+
inMergeQueue: boolean;
|
|
102
|
+
checks: ForgeCheck[];
|
|
103
|
+
/**
|
|
104
|
+
* Contexts branch protection requires (GitHub branch protection +
|
|
105
|
+
* rulesets); null when they could not be read.
|
|
106
|
+
*/
|
|
107
|
+
requiredContexts: string[] | null;
|
|
108
|
+
threads: ForgeThread[];
|
|
109
|
+
/** True when a list was cut at the page cap (the counts are lower bounds). */
|
|
110
|
+
truncated: boolean;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** The checks on the latest commit of a branch (the merge guard reads the base's). */
|
|
114
|
+
export type RefChecks = {
|
|
115
|
+
/** The commit the checks belong to; null when the forge did not say. */
|
|
116
|
+
sha: string | null;
|
|
117
|
+
/** `required` is set from branch protection when `requiredKnown`; else it is null on GitHub. */
|
|
118
|
+
checks: ForgeCheck[];
|
|
119
|
+
/** True when the branch's required checks could be read (GitLab gates on the whole pipeline). */
|
|
120
|
+
requiredKnown: boolean;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type JobRef = { jobId: number; scope: string | null };
|
|
124
|
+
|
|
125
|
+
export type RerunTarget =
|
|
126
|
+
| { kind: "failed_in_run"; runId: number; scope: string | null }
|
|
127
|
+
| { kind: "job"; jobId: number; scope: string | null };
|
|
128
|
+
|
|
129
|
+
export type Identity = {
|
|
130
|
+
login: string | null;
|
|
131
|
+
expected: string | null;
|
|
132
|
+
matches: boolean | null;
|
|
133
|
+
/** Why the login could not be checked (e.g. an app token without /user). */
|
|
134
|
+
note?: string;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type MergeMethod = "squash" | "merge" | "rebase";
|
|
138
|
+
|
|
139
|
+
/** What `workit pr create` asks the forge for (S11). */
|
|
140
|
+
export type CreatePrInput = {
|
|
141
|
+
/** Source branch in the push (head) repository. */
|
|
142
|
+
head: string;
|
|
143
|
+
/** owner/name (GitHub) or group/project (GitLab) of the push repository. */
|
|
144
|
+
headRepo: string;
|
|
145
|
+
/** GitLab: the push project id (MRs from a fork are opened from it). */
|
|
146
|
+
headProjectId: number | null;
|
|
147
|
+
base: string;
|
|
148
|
+
title: string;
|
|
149
|
+
body: string;
|
|
150
|
+
draft: boolean;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export type MergeOptions = {
|
|
154
|
+
sha: string;
|
|
155
|
+
method: MergeMethod;
|
|
156
|
+
/** Squash only: the commit message the forge writes instead of its default. */
|
|
157
|
+
squashMessage?: { subject: string; body: string };
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export type MergeResult = {
|
|
161
|
+
/** The merge (or squash/rebase tip) commit the forge reported, when it did. */
|
|
162
|
+
mergeSha: string | null;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** The editable fields of a PR/MR (`workit pr ready|edit`). */
|
|
166
|
+
export type PrMeta = {
|
|
167
|
+
number: number;
|
|
168
|
+
url: string;
|
|
169
|
+
state: PrState;
|
|
170
|
+
draft: boolean;
|
|
171
|
+
title: string;
|
|
172
|
+
base: string;
|
|
173
|
+
headBranch: string;
|
|
174
|
+
/** The author's login; null when the forge does not say. */
|
|
175
|
+
author: string | null;
|
|
176
|
+
/** The head comes from another repository than the PR's; null when unknown. */
|
|
177
|
+
fork: boolean | null;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** What `workit pr edit` changes; unset fields are left alone. */
|
|
181
|
+
export type PrEdit = {
|
|
182
|
+
title?: string;
|
|
183
|
+
body?: string;
|
|
184
|
+
base?: string;
|
|
185
|
+
addLabels: string[];
|
|
186
|
+
removeLabels: string[];
|
|
187
|
+
/** Logins; on GitHub `org/team` requests a team. */
|
|
188
|
+
addReviewers: string[];
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/** A GitHub release or GitLab release for a tag. */
|
|
192
|
+
export type ForgeRelease = { tag: string; url: string | null };
|
|
193
|
+
|
|
194
|
+
/** What `workit release finish` asks the forge for: a release on an existing tag. */
|
|
195
|
+
export type CreateReleaseInput = { tag: string; name: string; body: string };
|
|
196
|
+
|
|
197
|
+
/** A repository as the forge sees it: id and the repo it was forked from. */
|
|
198
|
+
export type RepoInfo = { id: number | null; parent: string | null };
|
|
199
|
+
|
|
200
|
+
export interface Forge {
|
|
201
|
+
kind: ForgeKind;
|
|
202
|
+
apiHost: string;
|
|
203
|
+
/** The base repository PRs/MRs live in. */
|
|
204
|
+
repo: string;
|
|
205
|
+
identity(expected: string | null): ForgeResult<Identity>;
|
|
206
|
+
repoInfo(repo: string): ForgeResult<RepoInfo>;
|
|
207
|
+
findPr(head: string, match: PrMatch): ForgeResult<PrRef | null>;
|
|
208
|
+
prStatus(n: number): ForgeResult<ForgePrStatus>;
|
|
209
|
+
/** The checks on the head commit of branch `branch`; empty when CI has not reported for it. */
|
|
210
|
+
branchChecks(branch: string): ForgeResult<RefChecks>;
|
|
211
|
+
jobLogTail(job: JobRef, lines: number): ForgeResult<string[]>;
|
|
212
|
+
rerun(target: RerunTarget): ForgeResult<void>;
|
|
213
|
+
/** Open a PR/MR; `headSha` is the head the forge reports right after creation. */
|
|
214
|
+
createPr(input: CreatePrInput): ForgeResult<PrRef>;
|
|
215
|
+
/**
|
|
216
|
+
* Merge with the head guard: the forge refuses (`blocked`, head_moved) when
|
|
217
|
+
* the PR head is no longer `sha`.
|
|
218
|
+
*/
|
|
219
|
+
merge(n: number, options: MergeOptions): ForgeResult<MergeResult>;
|
|
220
|
+
/** Retarget a PR/MR to another base branch (S12 stacks). */
|
|
221
|
+
updateBase(n: number, base: string): ForgeResult<void>;
|
|
222
|
+
/** Title, draft flag, base and state of one PR/MR. */
|
|
223
|
+
prMeta(n: number): ForgeResult<PrMeta>;
|
|
224
|
+
/** Numbers of the open PRs/MRs that target `branch` as their base (stack children). */
|
|
225
|
+
openPrsInto(branch: string): ForgeResult<number[]>;
|
|
226
|
+
/** Mark a draft ready (`draft: false`) or convert it back to a draft. */
|
|
227
|
+
setDraft(n: number, draft: boolean): ForgeResult<void>;
|
|
228
|
+
editPr(n: number, edit: PrEdit): ForgeResult<void>;
|
|
229
|
+
/** Reply to a review thread (GitHub review thread id, GitLab discussion id). */
|
|
230
|
+
replyThread(n: number, thread: string, body: string): ForgeResult<{ url: string | null }>;
|
|
231
|
+
resolveThread(n: number, thread: string): ForgeResult<void>;
|
|
232
|
+
/** One review thread of PR `n` read directly: null when it is not on that PR. */
|
|
233
|
+
threadState(n: number, thread: string): ForgeResult<"open" | "resolved" | null>;
|
|
234
|
+
/** The release for `tag`, or null when the forge has none. */
|
|
235
|
+
findRelease(tag: string): ForgeResult<ForgeRelease | null>;
|
|
236
|
+
/** Publish a release on an existing tag (never creates the tag). */
|
|
237
|
+
createRelease(input: CreateReleaseInput): ForgeResult<ForgeRelease>;
|
|
238
|
+
/** Bodies of the first page of comments on issue `n` of the base repository. */
|
|
239
|
+
issueComments(n: number): ForgeResult<string[]>;
|
|
240
|
+
/** Comment on issue `n` of the base repository (the PR backlink). */
|
|
241
|
+
commentIssue(n: number, body: string): ForgeResult<void>;
|
|
242
|
+
}
|
|
@@ -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
|
+
}
|