@webpieces/repo-workflow-core 0.0.1
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 +31 -0
- package/src/agent-worktree-lock.d.ts +21 -0
- package/src/agent-worktree-lock.js +77 -0
- package/src/agent-worktree-lock.js.map +1 -0
- package/src/branch-archiver.d.ts +70 -0
- package/src/branch-archiver.js +157 -0
- package/src/branch-archiver.js.map +1 -0
- package/src/branch-identity.d.ts +6 -0
- package/src/branch-identity.js +59 -0
- package/src/branch-identity.js.map +1 -0
- package/src/branch-mutation-log.d.ts +77 -0
- package/src/branch-mutation-log.js +221 -0
- package/src/branch-mutation-log.js.map +1 -0
- package/src/branch-reaper.d.ts +122 -0
- package/src/branch-reaper.js +261 -0
- package/src/branch-reaper.js.map +1 -0
- package/src/harness-agent-activity.d.ts +154 -0
- package/src/harness-agent-activity.js +346 -0
- package/src/harness-agent-activity.js.map +1 -0
- package/src/index.d.ts +14 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/main-sync-file.d.ts +121 -0
- package/src/main-sync-file.js +281 -0
- package/src/main-sync-file.js.map +1 -0
- package/src/main-sync-status.d.ts +197 -0
- package/src/main-sync-status.js +555 -0
- package/src/main-sync-status.js.map +1 -0
- package/src/merged-branch-verdicts.d.ts +189 -0
- package/src/merged-branch-verdicts.js +251 -0
- package/src/merged-branch-verdicts.js.map +1 -0
- package/src/merged-branches.d.ts +140 -0
- package/src/merged-branches.js +521 -0
- package/src/merged-branches.js.map +1 -0
- package/src/review-identity-stamp.d.ts +82 -0
- package/src/review-identity-stamp.js +172 -0
- package/src/review-identity-stamp.js.map +1 -0
- package/src/worktree-lock-verdicts.d.ts +149 -0
- package/src/worktree-lock-verdicts.js +225 -0
- package/src/worktree-lock-verdicts.js.map +1 -0
- package/src/worktree-reaper.d.ts +116 -0
- package/src/worktree-reaper.js +318 -0
- package/src/worktree-reaper.js.map +1 -0
- package/src/worktrees.d.ts +113 -0
- package/src/worktrees.js +260 -0
- package/src/worktrees.js.map +1 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { AtomicFile } from '@webpieces/tooling-common';
|
|
2
|
+
import { MAIN_SYNC_STATUS_VERSION, MainSyncFileStore, MainSyncStatus, MainSyncStatusFile, PullRequestIndex } from './main-sync-file';
|
|
3
|
+
import { DotWebpieces } from '@webpieces/tooling-common';
|
|
4
|
+
import { WorktreeService } from './worktrees';
|
|
5
|
+
export { MainSyncStatus, MainSyncStatusFile, PullRequestIndex, MAIN_SYNC_STATUS_VERSION };
|
|
6
|
+
export declare const DEFAULT_HANG_TIMEOUT_MINUTES = 5;
|
|
7
|
+
export declare class MainSyncLock {
|
|
8
|
+
state: string;
|
|
9
|
+
started: number;
|
|
10
|
+
pid: number;
|
|
11
|
+
constructor(state: string, started: number, pid?: number);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Reads/writes the main-sync cache + lock and computes the slow "is my branch healthy vs origin/main?"
|
|
15
|
+
* status. `@injectable(bindingScopeValues.Singleton)` so it's injectable and drawn in consumers’ DI designs.
|
|
16
|
+
*/
|
|
17
|
+
export declare class MainSyncStatusService {
|
|
18
|
+
private readonly dotDir;
|
|
19
|
+
private readonly atomicFile;
|
|
20
|
+
private readonly store;
|
|
21
|
+
private readonly worktrees;
|
|
22
|
+
constructor(dotDir?: DotWebpieces, atomicFile?: AtomicFile, store?: MainSyncFileStore, worktrees?: WorktreeService);
|
|
23
|
+
/**
|
|
24
|
+
* SHARED scope: `<primary>/.webpieces/main-sync-status.json`, one per repo.
|
|
25
|
+
*
|
|
26
|
+
* It is the OUTPUT of the single-flight refresher below, so it has to live where the lock lives —
|
|
27
|
+
* one `.git`, one `origin/main`, one fetch, one answer. A per-worktree copy under single-flight
|
|
28
|
+
* would only ever be written for whichever worktree won the lock anyway.
|
|
29
|
+
*
|
|
30
|
+
* FORMERLY a known defect, now fixed: the file's CONTENT used to be keyed to ONE branch, and every
|
|
31
|
+
* reader fails OPEN when the cached branch is not the branch being judged (`stale-cross-branch-cache
|
|
32
|
+
* (fail-open)`). With several worktrees on several branches, only the winner of the lock was ever
|
|
33
|
+
* described, so every other worktree's guards abstained — and which one was armed thrashed as the
|
|
34
|
+
* lock changed hands. The file is now a MAP of branch -> status (see MainSyncStatusFile): the single
|
|
35
|
+
* winning refresher enumerates every worktree and records all of their branches in one atomic write,
|
|
36
|
+
* so one fetch arms everyone. Single-flight is unchanged; only what the winner writes changed.
|
|
37
|
+
*/
|
|
38
|
+
mainSyncStatusPath(repoRoot: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* SHARED scope — the single-flight lock for the whole repo.
|
|
41
|
+
*
|
|
42
|
+
* This is the fix for the filed bug "the detached main-sync refresher's `git fetch` races the
|
|
43
|
+
* agent's foreground `git fetch`/`git pull` and corrupts `.git/FETCH_HEAD`". The lock was already
|
|
44
|
+
* atomic (O_CREAT|O_EXCL) and already serialised refreshers against each other — but only WITHIN
|
|
45
|
+
* one worktree, so N worktrees meant N locks and up to N concurrent `git fetch`es against the ONE
|
|
46
|
+
* shared `.git`, which is exactly how a duplicate `for-merge` line lands in FETCH_HEAD. There is
|
|
47
|
+
* one `.git`, so there is now one lock: at most one refresher in flight across all worktrees.
|
|
48
|
+
*
|
|
49
|
+
* Stale-lock handling is unchanged and still required (a lock nobody can clear is worse than the
|
|
50
|
+
* race): the holder records pid + start epoch, and `tryAcquireMainSyncLock` reclaims it once
|
|
51
|
+
* `isRefreshInProgress` proves the holder is finished, hung past hangTimeoutMinutes, or dead.
|
|
52
|
+
*/
|
|
53
|
+
mainSyncLockPath(repoRoot: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* The cached status FOR ONE BRANCH — the only read the four main-sync guards perform.
|
|
56
|
+
*
|
|
57
|
+
* `branch` is mandatory and is the branch being judged. A branch the last refresh never saw
|
|
58
|
+
* returns null, which every guard already treats as `no-sync-cache (fail-open)`, so an unknown
|
|
59
|
+
* branch degrades exactly as a missing file did. Any error (missing, truncated, malformed JSON)
|
|
60
|
+
* also returns null — this must never throw on the blocking path.
|
|
61
|
+
*/
|
|
62
|
+
readMainSyncStatus(repoRoot: string, branch: string): MainSyncStatus | null;
|
|
63
|
+
readMainSyncStatusFile(repoRoot: string): MainSyncStatusFile | null;
|
|
64
|
+
/**
|
|
65
|
+
* Merge ONE branch's entry into the existing map, atomically, leaving siblings untouched.
|
|
66
|
+
*
|
|
67
|
+
* Read-modify-write rather than replace: this is the writer used by everyone who is NOT the
|
|
68
|
+
* single-flight refresher (notably pr-gate's post-merge stamp, which holds no lock and knows about
|
|
69
|
+
* exactly one branch). Replacing the document from there would erase every other worktree's entry
|
|
70
|
+
* and silently disarm their guards until the next refresh.
|
|
71
|
+
*/
|
|
72
|
+
writeMainSyncStatus(repoRoot: string, status: MainSyncStatus): void;
|
|
73
|
+
/**
|
|
74
|
+
* Replace the WHOLE document — only the winning refresher may do this.
|
|
75
|
+
*
|
|
76
|
+
* ATOMIC (temp file + `rename()` in the same directory). The refresher writes this file while
|
|
77
|
+
* guards on the blocking path read it, possibly from another worktree; a plain `writeFileSync`
|
|
78
|
+
* truncates first, so a reader landing in that window gets a torn document. See AtomicFile.
|
|
79
|
+
*/
|
|
80
|
+
writeMainSyncStatusFile(repoRoot: string, file: MainSyncStatusFile): void;
|
|
81
|
+
readMainSyncLock(repoRoot: string): MainSyncLock | null;
|
|
82
|
+
writeMainSyncLock(repoRoot: string, lock: MainSyncLock): void;
|
|
83
|
+
isLockStale(lock: MainSyncLock, hangTimeoutMinutes: number, now?: number): boolean;
|
|
84
|
+
isRefreshInProgress(repoRoot: string, hangTimeoutMinutes: number, now?: number): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* ATOMICALLY take the refresher lock. Returns the held lock, or null when someone else holds it.
|
|
87
|
+
*
|
|
88
|
+
* Replaces the check-then-write pair (`isRefreshInProgress` then `writeMainSyncLock`), whose gap
|
|
89
|
+
* let two detached refreshers both pass the check and then both run `git fetch` at once — the
|
|
90
|
+
* concurrency this lock exists to prevent. The create uses the `wx` flag (O_CREAT|O_EXCL), so of
|
|
91
|
+
* N racing refreshers exactly one creates the file.
|
|
92
|
+
*
|
|
93
|
+
* A lock file left behind by a FINISHED, hung, or killed refresher is reclaimed: we only unlink
|
|
94
|
+
* and re-take it once `isRefreshInProgress` says the holder is provably not running, and we
|
|
95
|
+
* re-read afterwards to confirm the entry we see is ours before claiming the lock.
|
|
96
|
+
*/
|
|
97
|
+
tryAcquireMainSyncLock(repoRoot: string, hangTimeoutMinutes: number, now?: number, pid?: number): MainSyncLock | null;
|
|
98
|
+
private createExclusive;
|
|
99
|
+
inProcessLock(now?: number, pid?: number): MainSyncLock;
|
|
100
|
+
finishedLock(started: number): MainSyncLock;
|
|
101
|
+
/**
|
|
102
|
+
* The SLOW path for ONE branch: the branch checked out in `repoRoot`. Runs its own fetch and its
|
|
103
|
+
* own `gh` query, so it is the right entry point for a single-worktree caller and for tests.
|
|
104
|
+
* The refresher uses computeAllMainSyncStatuses instead — see there for why.
|
|
105
|
+
*/
|
|
106
|
+
computeMainSyncStatus(repoRoot: string): MainSyncStatus;
|
|
107
|
+
/**
|
|
108
|
+
* The SLOW path for EVERY branch this repo has checked out — what the detached refresher runs.
|
|
109
|
+
*
|
|
110
|
+
* The cache is shared by all worktrees but was written for one branch, so only one worktree's
|
|
111
|
+
* guards were ever armed. Enumerating the worktrees here and writing all of their branches in one
|
|
112
|
+
* atomic map arms every worktree off a single refresh.
|
|
113
|
+
*
|
|
114
|
+
* NETWORK COST DOES NOT SCALE WITH BRANCH COUNT, which is the whole reason this is a separate
|
|
115
|
+
* method: ONE `git fetch` and ONE repo-wide `gh pr list` for all N branches (the old code asked
|
|
116
|
+
* `gh` twice per branch, so looping it would have cost 2N round trips). Everything after that is
|
|
117
|
+
* local git in each worktree — merge-base, rev-parse, diff, ls-files.
|
|
118
|
+
*
|
|
119
|
+
* The map is rebuilt from the LIVE worktree set every refresh, so a deleted branch or removed
|
|
120
|
+
* worktree simply stops appearing. No pruning pass, no unbounded growth.
|
|
121
|
+
*/
|
|
122
|
+
computeAllMainSyncStatuses(repoRoot: string): MainSyncStatusFile;
|
|
123
|
+
private hasBranch;
|
|
124
|
+
/**
|
|
125
|
+
* One branch's status, computed in `worktreePath` (the tree that has it checked out — the
|
|
126
|
+
* working-tree overlap signals are only correct from there). Does NOT fetch and does NOT call
|
|
127
|
+
* `gh`: both are done once per refresh by the caller.
|
|
128
|
+
*/
|
|
129
|
+
private computeBranchStatus;
|
|
130
|
+
private stampForge;
|
|
131
|
+
private computeBranchStatusInner;
|
|
132
|
+
squashRecoverySteps(currentBranch: string): string[];
|
|
133
|
+
/**
|
|
134
|
+
* Synchronously stamp a clean "up to date with main" status — call right after a successful merge.
|
|
135
|
+
*
|
|
136
|
+
* This is the one writer that takes NO lock, and it knows about exactly one branch. It therefore
|
|
137
|
+
* goes through writeMainSyncStatus, which MERGES its entry into the existing map; writing a whole
|
|
138
|
+
* document from here would wipe every other worktree's entry.
|
|
139
|
+
*/
|
|
140
|
+
stampCleanMainSyncStatus(repoRoot: string): void;
|
|
141
|
+
/**
|
|
142
|
+
* Refresh `origin/main` WITHOUT writing `.git/FETCH_HEAD`.
|
|
143
|
+
*
|
|
144
|
+
* WHY the flag is not optional: this runs in a DETACHED background process while the agent is
|
|
145
|
+
* running its own foreground `git fetch` / `git pull` in the very same repo. `.git/FETCH_HEAD` is
|
|
146
|
+
* a single file that git takes no lock on, so two overlapping fetches interleave their writes and
|
|
147
|
+
* can leave the SAME `for-merge` line twice. `git pull` then reads two for-merge entries and dies
|
|
148
|
+
* with `fatal: Cannot fast-forward to multiple branches` — wedging the exact command the
|
|
149
|
+
* read-stale-guard tells the agent to run. `--no-write-fetch-head` (git >= 2.29) still updates the
|
|
150
|
+
* remote-tracking ref, which is all anything downstream reads (`origin/main`, merge-base), so
|
|
151
|
+
* nothing here needs FETCH_HEAD written at all.
|
|
152
|
+
*
|
|
153
|
+
* Older git rejects the flag; only then do we retry the plain form, accepting the old behaviour
|
|
154
|
+
* rather than losing the refresh entirely on a pre-2020 git.
|
|
155
|
+
*/
|
|
156
|
+
private fetchOriginMain;
|
|
157
|
+
private isUnknownGitOption;
|
|
158
|
+
private ensureDir;
|
|
159
|
+
private isProcessAlive;
|
|
160
|
+
private capture;
|
|
161
|
+
private gitBranch;
|
|
162
|
+
private localMainHash;
|
|
163
|
+
private commitDistance;
|
|
164
|
+
private changedFiles;
|
|
165
|
+
private featureChangedFiles;
|
|
166
|
+
/**
|
|
167
|
+
* ONE repo-wide `gh pr list` giving every branch's merged/open PR — replaces the two per-branch
|
|
168
|
+
* `--head <branch>` queries. `--limit 200` bounds it; a branch whose only PR is older than that
|
|
169
|
+
* falls out of the index and reads as "no PR", which is the fail-OPEN direction (no merged PR =
|
|
170
|
+
* no block), the same way a missing `gh` or an offline machine already degraded.
|
|
171
|
+
*/
|
|
172
|
+
private loadPullRequestIndex;
|
|
173
|
+
/**
|
|
174
|
+
* A `main` entry synthesised WITHOUT a worktree standing on main. Carries only the two hashes
|
|
175
|
+
* read-stale-guard / stale-main-bash-guard actually compare (originMain vs the live tree's
|
|
176
|
+
* ancestry) — never merged, never conflicting, so it can only ever produce the stale-main verdict.
|
|
177
|
+
*/
|
|
178
|
+
private mainOnlyStatus;
|
|
179
|
+
private benignStatus;
|
|
180
|
+
}
|
|
181
|
+
export declare function mainSyncStatusPath(repoRoot: string): string;
|
|
182
|
+
export declare function mainSyncLockPath(repoRoot: string): string;
|
|
183
|
+
export declare function readMainSyncStatus(repoRoot: string, branch: string): MainSyncStatus | null;
|
|
184
|
+
export declare function readMainSyncStatusFile(repoRoot: string): MainSyncStatusFile | null;
|
|
185
|
+
export declare function writeMainSyncStatus(repoRoot: string, status: MainSyncStatus): void;
|
|
186
|
+
export declare function writeMainSyncStatusFile(repoRoot: string, file: MainSyncStatusFile): void;
|
|
187
|
+
export declare function readMainSyncLock(repoRoot: string): MainSyncLock | null;
|
|
188
|
+
export declare function writeMainSyncLock(repoRoot: string, lock: MainSyncLock): void;
|
|
189
|
+
export declare function isLockStale(lock: MainSyncLock, hangTimeoutMinutes: number, now?: number): boolean;
|
|
190
|
+
export declare function isRefreshInProgress(repoRoot: string, hangTimeoutMinutes: number, now?: number): boolean;
|
|
191
|
+
export declare function tryAcquireMainSyncLock(repoRoot: string, hangTimeoutMinutes: number, now?: number, pid?: number): MainSyncLock | null;
|
|
192
|
+
export declare function inProcessLock(now?: number, pid?: number): MainSyncLock;
|
|
193
|
+
export declare function finishedLock(started: number): MainSyncLock;
|
|
194
|
+
export declare function computeMainSyncStatus(repoRoot: string): MainSyncStatus;
|
|
195
|
+
export declare function computeAllMainSyncStatuses(repoRoot: string): MainSyncStatusFile;
|
|
196
|
+
export declare function squashRecoverySteps(currentBranch: string): string[];
|
|
197
|
+
export declare function stampCleanMainSyncStatus(repoRoot: string): void;
|