@teambit/ci 1.0.505 → 1.0.508

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/ci.main.runtime.d.ts +3 -2
  2. package/dist/ci.main.runtime.js +5 -6
  3. package/dist/ci.main.runtime.js.map +1 -1
  4. package/dist/{preview-1787074471717.js → preview-1787160027560.js} +1 -1
  5. package/dist/sync/git-host-provider.d.ts +9 -0
  6. package/dist/sync/git-host-provider.js.map +1 -1
  7. package/dist/sync/github-client.d.ts +28 -0
  8. package/dist/sync/github-client.js +81 -3
  9. package/dist/sync/github-client.js.map +1 -1
  10. package/dist/sync/github-client.spec.js +249 -0
  11. package/dist/sync/github-client.spec.js.map +1 -1
  12. package/dist/sync/lane-sync-executor.d.ts +47 -3
  13. package/dist/sync/lane-sync-executor.js +191 -39
  14. package/dist/sync/lane-sync-executor.js.map +1 -1
  15. package/dist/sync/lane-sync-executor.spec.js +336 -0
  16. package/dist/sync/lane-sync-executor.spec.js.map +1 -1
  17. package/dist/sync/sync-planner.d.ts +12 -2
  18. package/dist/sync/sync-planner.js +17 -8
  19. package/dist/sync/sync-planner.js.map +1 -1
  20. package/dist/sync/sync-planner.spec.js +64 -0
  21. package/dist/sync/sync-planner.spec.js.map +1 -1
  22. package/dist/sync/sync-state.d.ts +34 -3
  23. package/dist/sync/sync-state.js +61 -3
  24. package/dist/sync/sync-state.js.map +1 -1
  25. package/dist/sync/sync-state.spec.js +54 -0
  26. package/dist/sync/sync-state.spec.js.map +1 -1
  27. package/package.json +15 -15
  28. package/sync/git-host-provider.ts +8 -0
  29. package/sync/github-client.spec.ts +160 -0
  30. package/sync/github-client.ts +90 -3
  31. package/sync/lane-sync-executor.spec.ts +291 -0
  32. package/sync/lane-sync-executor.ts +233 -50
  33. package/sync/sync-planner.spec.ts +41 -0
  34. package/sync/sync-planner.ts +31 -10
  35. package/sync/sync-state.spec.ts +48 -0
  36. package/sync/sync-state.ts +104 -4
@@ -92,9 +92,10 @@ export function hasSyncMarker(message: string): boolean {
92
92
 
93
93
  /**
94
94
  * Strict probe for "we wrote this commit": the marker alone on its own line. This is an input to branch
95
- * deletion and to the export-branch withhold (a branch whose tip is already our own commit settles
96
- * instead of re-exporting), so a developer merely quoting the marker must never count as authorship.
97
- * `\r?` tolerates CRLF; a recognition failure errs toward keeping the branch / re-attempting the export.
95
+ * DELETION only — the one decision no content probe can stand in for, because the lane it would compare
96
+ * against is gone. It must never decide convergence: a squash-merge's body quotes the squashed ledger
97
+ * messages verbatim, marker included, so a message can claim authorship the reconciler never had.
98
+ * `\r?` tolerates CRLF; a recognition failure errs toward keeping the branch.
98
99
  */
99
100
  export function isSyncAuthoredMessage(message: string): boolean {
100
101
  return new RegExp(`^${SYNC_COMMIT_MARKER.replace(/[[\]]/g, '\\$&')}\\r?$`, 'm').test(message);
@@ -160,6 +161,14 @@ export type BranchSyncState = {
160
161
  bitmap?: BranchBitmapState;
161
162
  /** Whether the branch carries commits on top of its bit state — work bit has not seen. */
162
163
  hasDevCommits: boolean;
164
+ /**
165
+ * Whether the state commit also touched files besides `.bitmap`. SUSPECTED work only: those files
166
+ * may already be inside the snap the `.bitmap` records, which git cannot tell — the executor's
167
+ * read-only status probe is what distinguishes real work. True for ANY other path (`docs/`, CI
168
+ * config, this reconciler's own source-bundling ledger commits): a spurious `true` re-probes, which
169
+ * writes nothing, so it can repeat without consequence.
170
+ */
171
+ stateCommitBundlesSources: boolean;
163
172
  /** The branch tip's full commit message (subject + body), for the sync-marker loop-guard probe. */
164
173
  tipMessage: string;
165
174
  /**
@@ -169,6 +178,40 @@ export type BranchSyncState = {
169
178
  tipSha?: string;
170
179
  };
171
180
 
181
+ /**
182
+ * The pure half of `hasUnsyncedWorkChanges` (ci.main.runtime.ts): whether a `bit status` result holds
183
+ * anything a snap/export would push. The unloadable categories count as work: `invalidComponents` AND
184
+ * `importPendingComponents` (`StatusMain` splits pending-import errors out of the invalid list) are
185
+ * both UNKNOWN — their sources may hold the branch's edits — and this answer gates a "converged,
186
+ * write nothing" exit, so not knowing must route to the snap (which fails loudly) rather than
187
+ * converge. Incoming-change categories (outdated, pending updates from main) deliberately do NOT
188
+ * count: they are the lane's or main's movement, which the planner routes, not branch work.
189
+ */
190
+ export function statusReportsUnsyncedWork(status: {
191
+ newComponents: unknown[];
192
+ modifiedComponents: unknown[];
193
+ stagedComponents: unknown[];
194
+ locallySoftRemoved: unknown[];
195
+ pendingUpdateDependents: unknown[];
196
+ mergePendingComponents: unknown[];
197
+ componentsDuringMergeState: unknown[];
198
+ invalidComponents: unknown[];
199
+ importPendingComponents: unknown[];
200
+ }): boolean {
201
+ const divergence = [
202
+ status.newComponents,
203
+ status.modifiedComponents,
204
+ status.stagedComponents,
205
+ status.locallySoftRemoved,
206
+ status.pendingUpdateDependents,
207
+ status.mergePendingComponents,
208
+ status.componentsDuringMergeState,
209
+ status.invalidComponents,
210
+ status.importPendingComponents,
211
+ ];
212
+ return divergence.some((components) => components.length > 0);
213
+ }
214
+
172
215
  /**
173
216
  * Whether a `rev-list --count` output reports commits. An unreadable count answers `true`: `false` is an
174
217
  * input to branch retirement, so not knowing must withhold a deletion rather than license one. Empty
@@ -200,7 +243,64 @@ export async function readBranchSyncState(
200
243
  const range = stateCommit ? `${stateCommit}..${revision}` : `origin/${defaultBranch}..${revision}`;
201
244
  const count = await git.raw(['rev-list', range, '--count']);
202
245
 
203
- return { stateCommit, bitmap, hasDevCommits: parseDevCommitCount(count), tipMessage, tipSha };
246
+ // Source edits can HIDE AT OR BELOW the state commit — bundled into it (the conflict-halt comment's
247
+ // resolve-by-hand recipe), or in an earlier commit that a later `.bitmap`-only commit covers — and
248
+ // `hasDevCommits` starts counting only after it, so they were invisible. Git file names alone cannot
249
+ // tell whether those sources are already inside the snap the `.bitmap` records (a dev who snapped,
250
+ // exported, and committed everything at once) or were never snapped at all — so this is SUSPECTED
251
+ // work, computed over the whole range since the PREVIOUS state commit (the last point a `.bitmap`
252
+ // write could have accounted for sources) and reported separately for the planner to probe.
253
+ // Deliberately content-only, per this module's header: this reconciler's own ledger commits have the
254
+ // same shape (import-lane and merge-diverged bundle already-exported sources) and its probe is a
255
+ // read, so a spurious `true` costs one status check — while consulting the tip's MESSAGE here read a
256
+ // squash-merge (whose body quotes the squashed ledger messages, marker included) as machine-authored
257
+ // and silently dropped the bundled work.
258
+ const stateCommitBundlesSources =
259
+ !parseDevCommitCount(count) &&
260
+ stateCommit !== undefined &&
261
+ stateCommit === tipSha &&
262
+ (await sourcesTouchedSincePreviousState(revision, defaultBranch));
263
+
264
+ return {
265
+ stateCommit,
266
+ bitmap,
267
+ hasDevCommits: parseDevCommitCount(count),
268
+ stateCommitBundlesSources,
269
+ tipMessage,
270
+ tipSha,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Whether any file besides `.bitmap` changed between the PREVIOUS `.bitmap`-writing commit (or, on a
276
+ * branch with only one, its fork point off the default branch) and the tip. A tree diff over the
277
+ * whole range, not a per-commit walk: a source edit in one commit followed by a `.bitmap`-only commit
278
+ * would be invisible to a single-commit check, and a later revert legitimately cancels out.
279
+ * Unreadable answers `true`: this feeds `stateCommitBundlesSources`, where not knowing must plan the
280
+ * probe, never declare convergence.
281
+ */
282
+ async function sourcesTouchedSincePreviousState(revision: string, defaultBranch: string): Promise<boolean> {
283
+ try {
284
+ const previousStateCommit = (
285
+ await git.raw(['log', revision, '--first-parent', '-n', '1', '--skip', '1', '--format=%H', '--', BIT_MAP])
286
+ ).trim();
287
+ const base = previousStateCommit || (await git.raw(['merge-base', `origin/${defaultBranch}`, revision])).trim();
288
+ if (!base) return true;
289
+ const names = await git.raw(['diff', '--name-only', `${base}..${revision}`]);
290
+ // Empty is legitimate here (a range whose edits cancel out), unlike a single state commit's diff.
291
+ return touchesBeyondBitmap(names);
292
+ } catch {
293
+ return true;
294
+ }
295
+ }
296
+
297
+ /** The pure half of `commitTouchesBeyondBitmap`, split out so it is testable without a real git log. */
298
+ export function touchesBeyondBitmap(rawNames: string): boolean {
299
+ return rawNames
300
+ .split('\n')
301
+ .map((name) => name.trim())
302
+ .filter(Boolean)
303
+ .some((name) => name !== BIT_MAP);
204
304
  }
205
305
 
206
306
  /**