@teambit/ci 1.0.642 → 1.0.644

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.
@@ -0,0 +1,132 @@
1
+ <!-- bit-ci-sync:start (written by `bit ci sync --init`; a re-run leaves this block as it is) -->
2
+
3
+ ## Bit ↔ Git sync: this repository mirrors `{{scope}}`
4
+
5
+ `bit ci sync` keeps this repository and the bit.cloud scope `{{scope}}` equal in both directions. **The rules in this section override the generic Bit guidance after it.**
6
+
7
+ ### The model
8
+
9
+ 1. **One repository mirrors one scope.** Components from other scopes arrive as package dependencies, never as source.
10
+ 2. **`{{branch}}` equals the scope's main.** The workflows own it. Never push to `{{branch}}` by hand.
11
+ 3. **Each lane is a branch plus a pull request.** The sync keeps them equal in both directions. By default, the branch has the lane's name. A lane name has no `/`, so the default mapping ignores a branch such as `feature/x`. The `branchPrefix` and `branches` keys of the `teambit.git/ci` sync config in `workspace.jsonc` can map a lane to a different branch name, such as `lane/x`. Read them before you name a branch.
12
+ 4. **Merging the pull request is the release.** `bit-release` merges the lane into the scope, exports new versions, commits the new `.bitmap` to `{{branch}}` and archives the lane. A lane with components of other scopes stays open until each of those scopes releases its slice.
13
+ 5. **The workflows do the work.** You change code, and then you either push a branch or export a lane.
14
+
15
+ ### Rules that override generic Bit guidance
16
+
17
+ - **Review happens on the GitHub pull request.** Do **not** submit a bit.cloud change request, even if other instructions say "always submit a change request". The sync opens the PR. Merging the PR is the release. A second review surface splits the reviewers and races the release.
18
+ - **One change, one scope.** Keep a lane inside the scope it is exported to. If the change also needs another scope (for example, a caller must pass a new argument), stop. Make the change backward compatible in this scope, and tell the user which follow-up change the other scope's repository needs. Cross-scope lanes are a preview (see below).
19
+ - **Never push to `{{branch}}`.** The workflows own it.
20
+
21
+ ### Make a change: pick a path
22
+
23
+ | You are… | Path | What you do | What appears |
24
+ | --------------------------------------------------------------- | ------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
25
+ | In this repository (Copilot, an IDE, a plain git user) | **Git-first** | branch → edit → `bit validate` → commit → push → open a PR | With the optional `bit-adopt-pr` workflow, the PR becomes a lane, and every push snaps onto it. Without it, the sync ignores the branch. |
26
+ | In any other Bit workspace (Claude Code with the Bit Cloud MCP) | **Bit-first** | `bit lane create` → edit → `bit validate` → `bit snap` → `bit export` | A branch and a PR named after the lane appear in this repository: within a minute with the scope webhook, else at the next scheduled sync. |
27
+
28
+ Both paths end with the same PR and the same release.
29
+
30
+ #### Git-first (in this repository)
31
+
32
+ This path needs the `bit-adopt-pr` workflow in `.github/workflows/`. On each push to a PR, it runs `bit ci pr --keep-lane`: this creates and exports a lane with the branch's name, and pushes the lane pointer back to the branch. `bit ci sync --init` does not write this workflow. Example: https://github.com/teambit/acme-modular-store/blob/main/.github/workflows/bit-adopt-pr.yml. Without it, a branch that has no lane stays out of the sync: use the Bit-first path.
33
+
34
+ ```bash
35
+ git switch -c add-gift-wrap # the lane gets the branch's name, so no "/"
36
+ bit install # once per clone
37
+ # edit the components; .bitmap says where each one lives
38
+ bit validate # lint + types + tests; never use tsc directly
39
+ git add -A && git commit -m "<message>"
40
+ git push -u origin add-gift-wrap
41
+ gh pr create --fill
42
+ ```
43
+
44
+ Commit `.bitmap` together with your source changes. Never edit `.bitmap` by hand. After the PR merges, run `git switch {{branch-sh}} && git pull` before you start the next change.
45
+
46
+ #### Bit-first (from another Bit workspace)
47
+
48
+ 1. Find components with the Bit Cloud MCP: `read_scope` / `search_components` for the owner, `read_components` for APIs, and `read_file` for source. Don't guess IDs. Also check who calls the component, so you know whether your change stays inside one scope.
49
+ 2. Run:
50
+
51
+ ```bash
52
+ bit init --default-scope {{scope}} # or reuse an existing workspace
53
+ bit import {{scope}}/<name> [...] # every component you will change, plus the chain up to its consumers
54
+ bit lane create <lane-name>
55
+ # edit
56
+ bit validate
57
+ bit snap -m "<message>"
58
+ bit export
59
+ ```
60
+
61
+ 3. Watch it arrive: `gh pr list -R {{repo}} --head <lane-name>`. Then tell the user the PR link. Don't submit a bit.cloud change request.
62
+
63
+ Headless or CI agents can call the Bit Cloud MCP with a bit.cloud token, without the OAuth step:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "bit-cloud": {
69
+ "type": "http",
70
+ "url": "https://mcp.bit.cloud/mcp",
71
+ "headers": { "Authorization": "Bearer ${BIT_CLOUD_TOKEN}" }
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ### Read a sync PR
78
+
79
+ - The PR body lists the mirrored components and the foreign ones (other scopes, "not mirrored").
80
+ - The bot's **run summary** comment lists the components that the sync last snapped onto the lane from this branch, with the branch and lane heads. A run that exports nothing does not change it.
81
+ - The run log says what each run did: `import-lane`, `export-branch`, `merge-diverged`, `adopt-branch`, `noop (converged)`. Runs: `gh run list --workflow bit-sync` and `gh run view <id> --log`.
82
+ - `Lane sync: <scope>/<lane>` PRs are lanes. `Bit sync: update to latest main scope versions` brings `{{branch}}` up to the scope. Merging that one releases nothing. Its branch is `bit-sync/main`, or the `mainSyncBranch` of the sync config. With `mainSync: "direct-push"` there is no such PR: the sync commits to `{{branch}}` itself.
83
+
84
+ ### Fix a halt (`bit-sync-conflict` label)
85
+
86
+ The lane and the branch changed the same lines. The lane stays paused while the label is on the PR. The bot's comment on the PR names the components and gives these steps:
87
+
88
+ ```bash
89
+ git fetch origin && git checkout '<branch>'
90
+ git merge --ff-only 'origin/<branch>' # stops if your local copy has commits that are not on origin: keep them, or reset to origin
91
+ bit install
92
+ bit lane import '<host-scope>/<lane>' # fetches the lane. If .bitmap is on main, it also switches to the lane
93
+ bit checkout head --manual --workspace-only # merges the lane's head into the files; conflicts appear as conflict markers
94
+ # resolve every conflicted file. When the intent is unclear, ask the user which side wins. Don't guess.
95
+ bit validate
96
+ git add -A && git commit -m "fix: resolve sync conflict"
97
+ git push origin '<branch>'
98
+ gh pr edit <pr> --remove-label bit-sync-conflict
99
+ gh workflow run bit-sync -f lane=<host-scope>/<lane>
100
+ ```
101
+
102
+ `--workspace-only` keeps the components of other scopes out of this repository. The next run adds the lane's new components of `{{scope}}`. Its log shows `merge-diverged` or `export-branch`, then a later run shows `noop (converged)`. Never resolve by force-pushing over the bot's commits. Never delete the lane to "start fresh".
103
+
104
+ ### Change several scopes at once (preview)
105
+
106
+ Prefer one lane per scope: one change, one repository, one PR.
107
+
108
+ A lane _can_ carry components from several scopes. Each scope's repository then mirrors only its own slice, consumes the other scopes' lane versions as packages, and releases its slice when its PR merges. The lane stays open until the last slice is released. Until automatic fan-out ships, only the repository of the lane's host scope hears about the lane. Start the others by hand:
109
+
110
+ ```bash
111
+ gh workflow run bit-sync -R <org>/<repo> -f lane=<host-scope>/<lane-name>
112
+ ```
113
+
114
+ ### Connect another scope to its own repository
115
+
116
+ ```bash
117
+ gh repo create <org>/<repo> --private --clone && cd <repo>
118
+ bit init --default-scope <owner>.<scope> --skip-interactive
119
+ bit import "<owner>.<scope>/**"
120
+ bit ci sync --init # writes the workflows, the sync config and this AGENTS.md section
121
+ ```
122
+
123
+ Then follow the steps `bit ci sync --init` prints. Some of them need a repository or organization admin. Guide: https://bit.cloud/teambit/git/guides/connect-scope-to-repository
124
+
125
+ ### Never
126
+
127
+ - Push to `{{branch}}`, or merge the main-sync PR and lane PRs out of order to "fix" history.
128
+ - Run `bit build`, `tsc`, `npm install` or `pnpm install` here. Use `bit validate` and `bit install`.
129
+ - Export a lane from another workspace while a Git-first PR is open for the same change. Pick one path.
130
+ - Remove a `bit-sync-conflict` label without resolving the conflict.
131
+
132
+ <!-- bit-ci-sync:end -->
@@ -3,6 +3,7 @@ import chalk from 'chalk';
3
3
  import { formatHint, formatItem, formatTitle, formatWarningSummary, joinSections, successSymbol } from '@teambit/cli';
4
4
  import path from 'path';
5
5
  import { parseGitHubRepo, isGitHubRemote, gitHubApiUrl, remoteHost } from './github-client';
6
+ import { AGENTS_MD_RELATIVE_PATH } from './agents-md';
6
7
 
7
8
  /**
8
9
  * `bit ci sync --init` — pure scaffolding logic, unit-testable without a live workspace/git checkout.
@@ -272,7 +273,10 @@ export function renderBitReleaseWorkflow(defaultBranch: string, wsDir?: string):
272
273
  export interface ScaffoldFileOutcome {
273
274
  /** relative to the workspace root, e.g. `.github/workflows/bit-sync.yml` */
274
275
  relativePath: string;
275
- status: 'written' | 'skipped';
276
+ /** `updated`: the file existed and `--init` added to it (the AGENTS.md sync section) */
277
+ status: 'written' | 'updated' | 'skipped';
278
+ /** what `--init` did, when the status alone does not say it */
279
+ note?: string;
276
280
  }
277
281
 
278
282
  // Never overwrites an existing file — `--init` must be safe to re-run after hand-edits.
@@ -344,8 +348,19 @@ const SETUP_GUIDE_URL = 'https://github.com/teambit/acme-modular-store#set-it-up
344
348
  const FILE_PURPOSE: Record<string, string> = {
345
349
  [WORKFLOW_RELATIVE_PATHS.sync]: 'syncs lanes and branches: on export, on push, and every 30 minutes',
346
350
  [WORKFLOW_RELATIVE_PATHS.release]: 'releases the scope when a pull request merges',
351
+ [AGENTS_MD_RELATIVE_PATH]: 'tells coding agents how the sync works in this repository',
347
352
  };
348
353
 
354
+ /** The note the report shows next to a file. */
355
+ function fileNote(file: ScaffoldFileOutcome): string {
356
+ if (file.note) return file.note;
357
+ if (file.status === 'written') return FILE_PURPOSE[file.relativePath] ?? '';
358
+ if (file.status === 'updated') return 'added the sync rules at the top';
359
+ return file.relativePath === AGENTS_MD_RELATIVE_PATH
360
+ ? 'already has the sync rules, left unchanged'
361
+ : 'already exists, left unchanged';
362
+ }
363
+
349
364
  export interface InitReportInput {
350
365
  /** the workspace's default scope, the one this repository mirrors */
351
366
  scopeId?: string;
@@ -368,7 +383,7 @@ export interface InitReportInput {
368
383
  repoHasCommits?: boolean;
369
384
  /** false when the workspace is not in a git repository yet: step 1 then starts with `git init` */
370
385
  inGitRepo?: boolean;
371
- /** scaffolded workflow files, with the path to show (relative to where the user stands) */
386
+ /** scaffolded files (the workflows, AGENTS.md), with the path to show (relative to where the user stands) */
372
387
  files: Array<ScaffoldFileOutcome & { displayPath: string }>;
373
388
  /** workspace.jsonc, as seen from where the user stands */
374
389
  configDisplayPath?: string;
@@ -490,8 +505,8 @@ export function renderInitReport(input: InitReportInput): string {
490
505
  const why = input.originUrl ? "the remote didn't name its default branch" : 'no remote names the default branch';
491
506
  header.push(
492
507
  formatWarningSummary(
493
- `${why}, so the workflows use "${defaultBranch}". If that's wrong,\n` +
494
- ` edit the branch filters in both workflow files.`
508
+ `${why}, so the workflows and AGENTS.md use "${defaultBranch}". If that's wrong,\n` +
509
+ ` edit the branch filters in both workflow files and the branch in the AGENTS.md sync section.`
495
510
  )
496
511
  );
497
512
  }
@@ -507,12 +522,7 @@ export function renderInitReport(input: InitReportInput): string {
507
522
  const width = Math.max(...input.files.map((f) => f.displayPath.length), configDisplayPath.length) + 2;
508
523
  const fileItem = (displayPath: string, note: string) => formatItem(`${displayPath.padEnd(width)}${formatHint(note)}`);
509
524
  const files = [
510
- ...input.files.map((file) =>
511
- fileItem(
512
- file.displayPath,
513
- file.status === 'written' ? (FILE_PURPOSE[file.relativePath] ?? '') : 'already exists, left unchanged'
514
- )
515
- ),
525
+ ...input.files.map((file) => fileItem(file.displayPath, fileNote(file))),
516
526
  fileItem(
517
527
  configDisplayPath,
518
528
  input.configAdded ? 'added "teambit.git/ci": { "sync": {} }' : 'already configures sync, left unchanged'
@@ -350,8 +350,13 @@ describe('haltCommentBody', () => {
350
350
  branch: LANE_NAME,
351
351
  laneId: `acme.shop/${LANE_NAME}`,
352
352
  });
353
- expect(body).to.include(`git fetch origin && git checkout '${LANE_NAME}'`);
353
+ expect(body).to.include(
354
+ `git fetch origin && git checkout '${LANE_NAME}' && git merge --ff-only 'origin/${LANE_NAME}'`
355
+ );
354
356
  expect(body).to.include(`bit lane import 'acme.shop/${LANE_NAME}'`);
357
+ // On a branch whose .bitmap is already on the lane, `bit lane import` only fetches: the merge is this step.
358
+ // --workspace-only: no component of another scope lands here. The next run adds new ones of this scope.
359
+ expect(body).to.include('bit checkout head --manual --workspace-only');
355
360
  expect(body).to.include(`git push origin '${LANE_NAME}'`);
356
361
  // the reason is prose, not a command, and the runbook still reads as one
357
362
  expect(body).to.include('merge conflicts in: acme.shop/comp1');
@@ -1932,8 +1932,9 @@ export function haltCommentBody({
1932
1932
  const resolution =
1933
1933
  note ??
1934
1934
  `To resolve locally:
1935
- git fetch origin && git checkout ${shellQuote(branch)}
1935
+ git fetch origin && git checkout ${shellQuote(branch)} && git merge --ff-only ${shellQuote(`origin/${branch}`)}
1936
1936
  bit lane import ${shellQuote(laneId)}
1937
+ bit checkout head --manual --workspace-only # merges the lane's head into the files; conflicts get conflict markers
1937
1938
  # resolve conflicts, commit the result, then:
1938
1939
  git push origin ${shellQuote(branch)}`;
1939
1940
  return `bit-git-sync could not reconcile this branch automatically: ${reason}
@@ -24,6 +24,7 @@ import { MainSyncExecutor } from './main-sync-executor';
24
24
  import { selectGitHostProvider } from './git-host-provider';
25
25
  import { gitRepoRoot, isNonContentPath, listRemoteBranches } from './git-ops';
26
26
  import { chooseInitDefaultBranch, deriveOwnerRepo, renderInitReport, scaffoldWorkflowFiles } from './init-scaffold';
27
+ import { UNKNOWN_REPO, scaffoldAgentsMd } from './agents-md';
27
28
 
28
29
  /**
29
30
  * Refuse a `--dry-run` whose plan would be computed by force-checking-out branches over uncommitted
@@ -209,8 +210,9 @@ export class SyncOrchestrator {
209
210
 
210
211
  /**
211
212
  * `bit ci sync --init` — one-command onboarding: scaffolds the two workflow files, adds the sync
212
- * config block to workspace.jsonc if missing, and returns the report of the steps left. Safe to
213
- * re-run: existing files are skipped, an existing config block is left alone; it never reconciles.
213
+ * config block to workspace.jsonc if missing, puts the sync rules for coding agents into AGENTS.md,
214
+ * and returns the report of the steps left. Safe to re-run: existing files are skipped, an existing
215
+ * config block or AGENTS.md sync section is left alone; it never reconciles.
214
216
  */
215
217
  private async syncInit(): Promise<string> {
216
218
  const remoteUrl = await git.remote(['get-url', 'origin']).catch(() => undefined);
@@ -249,6 +251,15 @@ export class SyncOrchestrator {
249
251
  return fromCwd && !path.isAbsolute(fromCwd) ? fromCwd : abs;
250
252
  };
251
253
  const configAdded = await this.ensureSyncConfigBlock();
254
+ // AGENTS.md belongs to the workspace, where agents run bit commands.
255
+ const ownerRepo = deriveOwnerRepo(origin);
256
+ const agentsOutcome = scaffoldAgentsMd(this.deps.workspace.path, {
257
+ scope: this.deps.workspace.defaultScope ?? '<owner>.<scope>',
258
+ repo: ownerRepo
259
+ ? `${ownerRepo.host === 'github.com' ? '' : `${ownerRepo.host}/`}${ownerRepo.owner}/${ownerRepo.repo}`
260
+ : UNKNOWN_REPO,
261
+ defaultBranch,
262
+ });
252
263
  // No commit yet: the first commit must take the whole workspace, not only the files --init wrote.
253
264
  const repoHasCommits = await git
254
265
  .raw(['rev-parse', '--verify', '--quiet', 'HEAD'])
@@ -260,6 +271,7 @@ export class SyncOrchestrator {
260
271
  const handled = [
261
272
  ...fileOutcomes.map((outcome) => path.join(scaffoldRoot, outcome.relativePath)),
262
273
  path.join(this.deps.workspace.path, 'workspace.jsonc'),
274
+ path.join(this.deps.workspace.path, agentsOutcome.relativePath),
263
275
  ];
264
276
  const toStage: string[] = [];
265
277
  const ignored: string[] = [];
@@ -291,7 +303,7 @@ export class SyncOrchestrator {
291
303
  // Returned, not logged: the command prints what `report()` returns.
292
304
  return renderInitReport({
293
305
  scopeId: this.deps.workspace.defaultScope,
294
- ownerRepo: deriveOwnerRepo(origin),
306
+ ownerRepo,
295
307
  originUrl: origin,
296
308
  defaultBranch,
297
309
  defaultBranchGuessed: !detectedBranch,
@@ -299,10 +311,13 @@ export class SyncOrchestrator {
299
311
  repoRootFromCwd: path.relative(process.cwd(), scaffoldRoot) || '.',
300
312
  repoHasCommits,
301
313
  inGitRepo: Boolean(repoRoot),
302
- files: fileOutcomes.map((outcome) => ({
303
- ...outcome,
304
- displayPath: displayPath(path.join(scaffoldRoot, outcome.relativePath)),
305
- })),
314
+ files: [
315
+ ...fileOutcomes.map((outcome) => ({
316
+ ...outcome,
317
+ displayPath: displayPath(path.join(scaffoldRoot, outcome.relativePath)),
318
+ })),
319
+ { ...agentsOutcome, displayPath: displayPath(path.join(this.deps.workspace.path, agentsOutcome.relativePath)) },
320
+ ],
306
321
  configDisplayPath: displayPath(path.join(this.deps.workspace.path, 'workspace.jsonc')),
307
322
  configAdded,
308
323
  ...(repoRoot ? { toStage, ignored } : {}),