@teambit/ci 1.0.485 → 1.0.487

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 (97) hide show
  1. package/ci.docs.mdx +344 -83
  2. package/commands/sync.cmd.ts +77 -0
  3. package/dist/ci.docs.mdx +344 -83
  4. package/dist/ci.main.runtime.d.ts +67 -6
  5. package/dist/ci.main.runtime.js +152 -25
  6. package/dist/ci.main.runtime.js.map +1 -1
  7. package/dist/commands/sync.cmd.d.ts +34 -0
  8. package/dist/commands/sync.cmd.js +57 -0
  9. package/dist/commands/sync.cmd.js.map +1 -0
  10. package/dist/git.d.ts +11 -1
  11. package/dist/git.js +11 -1
  12. package/dist/git.js.map +1 -1
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.js +26 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/{preview-1785858957533.js → preview-1785883495880.js} +1 -1
  17. package/dist/sync/git-host-provider.d.ts +47 -0
  18. package/dist/sync/git-host-provider.js +54 -0
  19. package/dist/sync/git-host-provider.js.map +1 -0
  20. package/dist/sync/git-host-provider.spec.d.ts +1 -0
  21. package/dist/sync/git-host-provider.spec.js +171 -0
  22. package/dist/sync/git-host-provider.spec.js.map +1 -0
  23. package/dist/sync/git-ops.d.ts +171 -0
  24. package/dist/sync/git-ops.js +354 -0
  25. package/dist/sync/git-ops.js.map +1 -0
  26. package/dist/sync/git-ops.spec.d.ts +1 -0
  27. package/dist/sync/git-ops.spec.js +476 -0
  28. package/dist/sync/git-ops.spec.js.map +1 -0
  29. package/dist/sync/github-client.d.ts +79 -0
  30. package/dist/sync/github-client.js +198 -0
  31. package/dist/sync/github-client.js.map +1 -0
  32. package/dist/sync/github-client.spec.d.ts +1 -0
  33. package/dist/sync/github-client.spec.js +205 -0
  34. package/dist/sync/github-client.spec.js.map +1 -0
  35. package/dist/sync/init-scaffold.d.ts +40 -0
  36. package/dist/sync/init-scaffold.js +326 -0
  37. package/dist/sync/init-scaffold.js.map +1 -0
  38. package/dist/sync/init-scaffold.spec.d.ts +1 -0
  39. package/dist/sync/init-scaffold.spec.js +169 -0
  40. package/dist/sync/init-scaffold.spec.js.map +1 -0
  41. package/dist/sync/lane-sync-executor.d.ts +256 -0
  42. package/dist/sync/lane-sync-executor.js +1249 -0
  43. package/dist/sync/lane-sync-executor.js.map +1 -0
  44. package/dist/sync/lane-sync-executor.spec.d.ts +1 -0
  45. package/dist/sync/lane-sync-executor.spec.js +416 -0
  46. package/dist/sync/lane-sync-executor.spec.js.map +1 -0
  47. package/dist/sync/main-sync-executor.d.ts +62 -0
  48. package/dist/sync/main-sync-executor.js +305 -0
  49. package/dist/sync/main-sync-executor.js.map +1 -0
  50. package/dist/sync/sync-config.d.ts +83 -0
  51. package/dist/sync/sync-config.js +187 -0
  52. package/dist/sync/sync-config.js.map +1 -0
  53. package/dist/sync/sync-config.spec.d.ts +1 -0
  54. package/dist/sync/sync-config.spec.js +254 -0
  55. package/dist/sync/sync-config.spec.js.map +1 -0
  56. package/dist/sync/sync-orchestrator.d.ts +76 -0
  57. package/dist/sync/sync-orchestrator.js +396 -0
  58. package/dist/sync/sync-orchestrator.js.map +1 -0
  59. package/dist/sync/sync-orchestrator.spec.d.ts +1 -0
  60. package/dist/sync/sync-orchestrator.spec.js +155 -0
  61. package/dist/sync/sync-orchestrator.spec.js.map +1 -0
  62. package/dist/sync/sync-planner.d.ts +60 -0
  63. package/dist/sync/sync-planner.js +114 -0
  64. package/dist/sync/sync-planner.js.map +1 -0
  65. package/dist/sync/sync-planner.spec.d.ts +1 -0
  66. package/dist/sync/sync-planner.spec.js +210 -0
  67. package/dist/sync/sync-planner.spec.js.map +1 -0
  68. package/dist/sync/sync-state.d.ts +94 -0
  69. package/dist/sync/sync-state.js +178 -0
  70. package/dist/sync/sync-state.js.map +1 -0
  71. package/dist/sync/sync-state.spec.d.ts +1 -0
  72. package/dist/sync/sync-state.spec.js +271 -0
  73. package/dist/sync/sync-state.spec.js.map +1 -0
  74. package/dist/sync/workspace-lane.d.ts +13 -0
  75. package/dist/sync/workspace-lane.js +27 -0
  76. package/dist/sync/workspace-lane.js.map +1 -0
  77. package/package.json +20 -15
  78. package/sync/git-host-provider.spec.ts +197 -0
  79. package/sync/git-host-provider.ts +89 -0
  80. package/sync/git-ops.spec.ts +506 -0
  81. package/sync/git-ops.ts +334 -0
  82. package/sync/github-client.spec.ts +174 -0
  83. package/sync/github-client.ts +209 -0
  84. package/sync/init-scaffold.spec.ts +142 -0
  85. package/sync/init-scaffold.ts +340 -0
  86. package/sync/lane-sync-executor.spec.ts +389 -0
  87. package/sync/lane-sync-executor.ts +1363 -0
  88. package/sync/main-sync-executor.ts +321 -0
  89. package/sync/sync-config.spec.ts +283 -0
  90. package/sync/sync-config.ts +214 -0
  91. package/sync/sync-orchestrator.spec.ts +120 -0
  92. package/sync/sync-orchestrator.ts +386 -0
  93. package/sync/sync-planner.spec.ts +126 -0
  94. package/sync/sync-planner.ts +99 -0
  95. package/sync/sync-state.spec.ts +263 -0
  96. package/sync/sync-state.ts +176 -0
  97. package/sync/workspace-lane.ts +20 -0
@@ -0,0 +1,340 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { parseGitHubRepo, isGitHubRemote } from './github-client';
4
+
5
+ /**
6
+ * `bit ci sync --init` — pure scaffolding logic, unit-testable without a live workspace/git checkout.
7
+ *
8
+ * TEMPLATE SOURCE OF TRUTH: `BIT_SYNC_WORKFLOW_RAW` / `BIT_RELEASE_WORKFLOW_RAW` are byte-for-byte
9
+ * copies of the canonical templates in the `bit-git-sync` action workspace
10
+ * (git-sync/git-sync/workflows/templates/) and MUST be kept in sync with them. The ONLY departure is
11
+ * the two `DEFAULT_BRANCH_MARKER` substitution points; everything else — CHANGE-ME comments, the
12
+ * placeholder action ref, `bit-sync/main` config defaults — stays untouched.
13
+ */
14
+
15
+ /** Where `scaffoldWorkflowFiles` writes each file, relative to the workspace root. */
16
+ export const WORKFLOW_RELATIVE_PATHS = {
17
+ sync: path.join('.github', 'workflows', 'bit-sync.yml'),
18
+ release: path.join('.github', 'workflows', 'bit-release.yml'),
19
+ } as const;
20
+
21
+ const BIT_SYNC_WORKFLOW_RAW = `# Copy this file into your repository at \`.github/workflows/bit-sync.yml\`,
22
+ # then review every line marked \`CHANGE-ME\` below -- the defaults assume a
23
+ # repository whose default branch is \`main\` and a workspace at the repo root.
24
+ #
25
+ # See the \`workflows.docs.mdx\` customer setup guide (component
26
+ # bitdev.git-sync/git-sync/workflows) for the full walk-through: the
27
+ # bit.cloud webhook wiring that drives \`repository_dispatch\`, the required
28
+ # secrets, and the \`teambit.git/ci\`.\`sync\` workspace config this workflow
29
+ # acts on.
30
+ name: bit-sync
31
+ on:
32
+ repository_dispatch:
33
+ # 'bit-export' is the real, empirically-verified bit.cloud webhook
34
+ # contract: a single dispatch type discriminated by
35
+ # client_payload.laneId (non-empty 'scope/lane' -> lane export, empty ->
36
+ # main export). The other three types are forward-compat aliases the
37
+ # event router also accepts, in case a webhook is configured to send the
38
+ # originally-assumed, more granular dispatch types instead.
39
+ types: [bit-export, bit-lane-export, bit-main-export, bit-lane-removed]
40
+ schedule:
41
+ # Scheduled reconcile. This is also the ONLY mechanism that picks up
42
+ # lane deletions -- bit.cloud has no "lane removed" webhook event.
43
+ - cron: '*/30 * * * *'
44
+ push:
45
+ # CHANGE-ME: \`main\` must be your repository's DEFAULT branch, and
46
+ # \`bit-sync/**\` must cover your configured \`sync.mainSyncBranch\`. Pushes
47
+ # to either are bit-sync's own output and must not re-trigger a sync.
48
+ branches-ignore: [main, 'bit-sync/**']
49
+ workflow_dispatch:
50
+ inputs:
51
+ lane: { description: Lane to sync (empty = all), required: false }
52
+
53
+ # The default GITHUB_TOKEN is READ-ONLY on repositories and organizations
54
+ # created after Feb 2023, and anywhere the org/repo default is set to
55
+ # "Read repository contents permission". Sync pushes branches and
56
+ # opens/updates PRs, so both scopes must be declared explicitly -- without
57
+ # this block the sync fails with a 403 on its first write.
58
+ permissions:
59
+ contents: write
60
+ pull-requests: write
61
+
62
+ concurrency:
63
+ # Serializes one lane's syncs so two runs never race on the same branch/PR.
64
+ #
65
+ # Each trigger needs a different expression, because the lane identity
66
+ # arrives in a different place every time:
67
+ # - repository_dispatch -> client_payload.laneId ('scope/lane'), or
68
+ # client_payload.lane for the alias types
69
+ # - push -> github.ref_name (the branch)
70
+ # - workflow_dispatch -> the \`lane\` input
71
+ # - schedule -> nothing lane-shaped; it reconciles everything
72
+ #
73
+ # github.ref_name is populated on schedule runs too (it is the default
74
+ # branch there), so it may only be consulted AFTER the event name has been
75
+ # checked. Reading it first -- as a bare \`... || github.ref_name || 'all'\`
76
+ # chain does -- makes the final fallback unreachable and lumps every cron
77
+ # run into the same group as pushes to the default branch.
78
+ #
79
+ # RESIDUAL RACE (accepted, by design): a webhook run groups as
80
+ # 'scope/my-lane' while a push to that lane's branch groups as 'my-lane', so
81
+ # two runs for the same lane can still overlap across those two triggers.
82
+ # GitHub expressions have no substring/split function, and bit.cloud's
83
+ # webhook payload template exposes only the full laneId, so the two keys
84
+ # cannot be normalized into one here. This is safe rather than merely
85
+ # tolerated: the reconciler is idempotent and the sync executor diffs
86
+ # against converged state, so a second overlapping run finds nothing left to
87
+ # do and no-ops. The concurrency group is an efficiency measure; it is not
88
+ # what makes the sync correct.
89
+ group: >-
90
+ bit-sync-\${{
91
+ github.event_name == 'repository_dispatch' && (github.event.client_payload.laneId || github.event.client_payload.lane || 'main-export')
92
+ || github.event_name == 'push' && github.ref_name
93
+ || github.event_name == 'workflow_dispatch' && (github.event.inputs.lane || 'reconcile-all')
94
+ || 'reconcile-all' }}
95
+ # Never cancel a sync mid-flight: it may already have pushed a branch and be
96
+ # partway through opening or updating its PR.
97
+ cancel-in-progress: false
98
+
99
+ jobs:
100
+ sync:
101
+ runs-on: ubuntu-latest
102
+ steps:
103
+ - uses: actions/checkout@v4
104
+ with:
105
+ fetch-depth: 0
106
+ # A PAT/App token so that pushes this workflow makes (e.g. a sync
107
+ # PR branch) can themselves trigger CI. Pushes authenticated with
108
+ # the default GITHUB_TOKEN do NOT trigger other workflow runs.
109
+ token: \${{ secrets.BIT_SYNC_GH_TOKEN || secrets.GITHUB_TOKEN }}
110
+ - uses: bit-tasks/init@v2
111
+ # Pinned to a commit SHA on purpose: this job holds write permission, and
112
+ # a tag can move. Bump the SHA to take a new version of the action.
113
+ - uses: teambit/bit-git-sync@66c0fdf9e34e45f70b4f397b38b2b01f79dd1f41
114
+ with:
115
+ # CHANGE-ME: keep this in sync with
116
+ # \`teambit.git/ci\`.\`sync.mainSyncBranch\` in your workspace.jsonc
117
+ # (the action's own default is shown here).
118
+ main-sync-branch: bit-sync/main
119
+ # Uncomment if your workspace.jsonc is NOT at the repository root --
120
+ # the action chdirs here before running anything.
121
+ # ws-dir: packages/my-workspace
122
+ env:
123
+ # Service account token. This account must be EXEMPT from the
124
+ # org's change-request/merge block, or bit.cloud rejects the
125
+ # sync-driven writes.
126
+ BIT_CONFIG_ACCESS_TOKEN: \${{ secrets.BIT_CONFIG_ACCESS_TOKEN }}
127
+ GITHUB_TOKEN: \${{ secrets.BIT_SYNC_GH_TOKEN || secrets.GITHUB_TOKEN }}
128
+ # Optional: git identity for the commits the sync creates. Defaults
129
+ # to bit-sync[bot] / bit-sync[bot]@users.noreply.github.com.
130
+ # GIT_USER_NAME: bit-sync[bot]
131
+ # GIT_USER_EMAIL: bit-sync[bot]@users.noreply.github.com
132
+ `;
133
+
134
+ const BIT_RELEASE_WORKFLOW_RAW = `# Copy this file into your repository at \`.github/workflows/bit-release.yml\`,
135
+ # then review every line marked \`CHANGE-ME\` below.
136
+ #
137
+ # See the \`workflows.docs.mdx\` customer setup guide (component
138
+ # bitdev.git-sync/git-sync/workflows) for the full walk-through.
139
+ name: bit-release
140
+ on:
141
+ pull_request:
142
+ types: [closed]
143
+ # CHANGE-ME: your repository's DEFAULT branch. \`bit ci merge\` releases
144
+ # from the default branch, so only PRs merged INTO it should trigger a
145
+ # release -- a stacked PR landing on another feature branch has not
146
+ # reached the default branch yet. The action's event router enforces the
147
+ # same rule from \`pull_request.base.ref\`; this filter means a run is never
148
+ # even queued for a non-default base.
149
+ branches: [main]
150
+
151
+ # The default GITHUB_TOKEN is READ-ONLY on repositories and organizations
152
+ # created after Feb 2023, and anywhere the org/repo default is set to
153
+ # "Read repository contents permission". The release run pushes commits/tags
154
+ # and updates PRs, so both scopes must be declared explicitly.
155
+ permissions:
156
+ contents: write
157
+ pull-requests: write
158
+
159
+ concurrency:
160
+ group: bit-release-\${{ github.event.pull_request.number }}
161
+ # Never cancel a release mid-flight.
162
+ cancel-in-progress: false
163
+
164
+ jobs:
165
+ release:
166
+ runs-on: ubuntu-latest
167
+ # Merged main-sync PRs must NOT trigger a release: the scope is already
168
+ # ahead of this merge, there is nothing new to release. The event
169
+ # router also skips this case (routePullRequest checks
170
+ # prHeadRef === mainSyncBranch) -- this \`if:\` is defense in depth so the
171
+ # job doesn't even start.
172
+ # CHANGE-ME: the branch literal below MUST match your workspace's
173
+ # configured \`teambit.git/ci\`.\`sync.mainSyncBranch\`.
174
+ if: github.event.pull_request.merged == true && github.event.pull_request.head.ref != 'bit-sync/main'
175
+ steps:
176
+ - uses: actions/checkout@v4
177
+ with:
178
+ fetch-depth: 0
179
+ token: \${{ secrets.BIT_SYNC_GH_TOKEN || secrets.GITHUB_TOKEN }}
180
+ - uses: bit-tasks/init@v2
181
+ # Pinned to a commit SHA on purpose: this job holds write permission, and
182
+ # a tag can move. Bump the SHA to take a new version of the action.
183
+ - uses: teambit/bit-git-sync@66c0fdf9e34e45f70b4f397b38b2b01f79dd1f41
184
+ with:
185
+ # CHANGE-ME: keep this in sync with
186
+ # \`teambit.git/ci\`.\`sync.mainSyncBranch\` AND with the \`if:\`
187
+ # condition above.
188
+ main-sync-branch: bit-sync/main
189
+ # Uncomment if your workspace.jsonc is NOT at the repository root --
190
+ # the action chdirs here before running anything.
191
+ # ws-dir: packages/my-workspace
192
+ env:
193
+ BIT_CONFIG_ACCESS_TOKEN: \${{ secrets.BIT_CONFIG_ACCESS_TOKEN }}
194
+ GITHUB_TOKEN: \${{ secrets.BIT_SYNC_GH_TOKEN || secrets.GITHUB_TOKEN }}
195
+ # Optional: git identity for the commits the release creates.
196
+ # Defaults to bit-sync[bot] / bit-sync[bot]@users.noreply.github.com.
197
+ # GIT_USER_NAME: bit-sync[bot]
198
+ # GIT_USER_EMAIL: bit-sync[bot]@users.noreply.github.com
199
+ `;
200
+
201
+ /**
202
+ * The exact substring in each template that encodes the repository's default branch. Exact substring,
203
+ * never a blanket `main` regex — that would also corrupt `bit-main-export` and `bit-sync/main`.
204
+ */
205
+ const DEFAULT_BRANCH_MARKER = {
206
+ sync: "branches-ignore: [main, 'bit-sync/**']",
207
+ release: 'branches: [main]',
208
+ } as const;
209
+
210
+ /**
211
+ * A YAML single-quoted scalar of `value` (the only escape inside single quotes is a doubled `''`).
212
+ * Both substitution points land inside a YAML flow sequence, where `,` and `]` are structural — and a
213
+ * branch name may legally contain both, so quoting is unconditional.
214
+ */
215
+ function yamlSingleQuoted(value: string): string {
216
+ return `'${value.replace(/'/g, "''")}'`;
217
+ }
218
+
219
+ /**
220
+ * Replace the default-branch literal inside `marker` with `defaultBranch`, inside `raw`. Throws when
221
+ * the marker has drifted out of the template — a reworded canonical workflow must fail loudly, not
222
+ * ship a scaffold that still says `main`.
223
+ */
224
+ function substituteDefaultBranch(raw: string, marker: string, defaultBranch: string): string {
225
+ if (!raw.includes(marker)) {
226
+ throw new Error(
227
+ `init-scaffold: expected marker "${marker}" not found in its workflow template — the canonical ` +
228
+ `template (bitdev.git-sync/git-sync/workflows) has drifted from BIT_SYNC_WORKFLOW_RAW / ` +
229
+ `BIT_RELEASE_WORKFLOW_RAW; update the copy and this substitution point together`
230
+ );
231
+ }
232
+ // Function replacements, not strings: `$&`/`$'`/`$$` are special in string replacements and `$` is
233
+ // git-legal in a branch name. A function replacement is inserted verbatim.
234
+ const substitutedMarker = marker.replace('main', () => yamlSingleQuoted(defaultBranch));
235
+ return raw.replace(marker, () => substitutedMarker);
236
+ }
237
+
238
+ /** Render `bit-sync.yml` with the repository's actual default branch substituted. */
239
+ export function renderBitSyncWorkflow(defaultBranch: string): string {
240
+ return substituteDefaultBranch(BIT_SYNC_WORKFLOW_RAW, DEFAULT_BRANCH_MARKER.sync, defaultBranch);
241
+ }
242
+
243
+ /** Render `bit-release.yml` with the repository's actual default branch substituted. */
244
+ export function renderBitReleaseWorkflow(defaultBranch: string): string {
245
+ return substituteDefaultBranch(BIT_RELEASE_WORKFLOW_RAW, DEFAULT_BRANCH_MARKER.release, defaultBranch);
246
+ }
247
+
248
+ export interface ScaffoldFileOutcome {
249
+ /** relative to the workspace root, e.g. `.github/workflows/bit-sync.yml` */
250
+ relativePath: string;
251
+ status: 'written' | 'skipped';
252
+ }
253
+
254
+ // Never overwrites an existing file — `--init` must be safe to re-run after hand-edits.
255
+ function writeIfAbsent(workspaceDir: string, relativePath: string, content: string): ScaffoldFileOutcome {
256
+ const absPath = path.join(workspaceDir, relativePath);
257
+ if (fs.existsSync(absPath)) {
258
+ return { relativePath, status: 'skipped' };
259
+ }
260
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
261
+ fs.writeFileSync(absPath, content);
262
+ return { relativePath, status: 'written' };
263
+ }
264
+
265
+ /** Scaffold both workflow files under `workspaceDir`. Never overwrites; creates `.github/workflows/` as needed. */
266
+ export function scaffoldWorkflowFiles(workspaceDir: string, defaultBranch: string): ScaffoldFileOutcome[] {
267
+ return [
268
+ writeIfAbsent(workspaceDir, WORKFLOW_RELATIVE_PATHS.sync, renderBitSyncWorkflow(defaultBranch)),
269
+ writeIfAbsent(workspaceDir, WORKFLOW_RELATIVE_PATHS.release, renderBitReleaseWorkflow(defaultBranch)),
270
+ ];
271
+ }
272
+
273
+ export interface OwnerRepo {
274
+ owner: string;
275
+ repo: string;
276
+ }
277
+
278
+ /**
279
+ * Derive `{owner, repo}` from the `origin` remote when it is a parseable GitHub remote, else
280
+ * undefined (the checklist falls back to a placeholder). Reuses `github-client.ts`'s parsing so the
281
+ * two never disagree.
282
+ */
283
+ export function deriveOwnerRepo(remoteUrl: string | undefined): OwnerRepo | undefined {
284
+ if (!remoteUrl || !isGitHubRemote(remoteUrl)) return undefined;
285
+ const ownerSlashRepo = parseGitHubRepo(remoteUrl);
286
+ if (!ownerSlashRepo) return undefined;
287
+ const [owner, repo] = ownerSlashRepo.split('/');
288
+ if (!owner || !repo) return undefined;
289
+ return { owner, repo };
290
+ }
291
+
292
+ /**
293
+ * The bit.cloud custom webhook payload template, verified byte-perfect against a live delivery.
294
+ * Lane vs. main export is discriminated by whether `laneId` is empty.
295
+ */
296
+ const WEBHOOK_PAYLOAD_TEMPLATE =
297
+ '{"event_type":"bit-export","client_payload":{"laneId":"{{laneId}}","componentIds":"{{componentIds}}","owner":"{{owner}}","actor":"{{username}}"}}';
298
+
299
+ /**
300
+ * The manual-steps checklist `bit ci sync --init` prints: everything the command itself cannot do
301
+ * (secrets live in GitHub settings; the webhook lives on bit.cloud).
302
+ */
303
+ export function renderInitChecklist(ownerRepo: OwnerRepo | undefined): string {
304
+ const repoSegment = ownerRepo ? `${ownerRepo.owner}/${ownerRepo.repo}` : '<owner>/<repo>';
305
+ const dispatchUrl = `https://api.github.com/repos/${repoSegment}/dispatches`;
306
+ return [
307
+ '',
308
+ 'Remaining manual steps (bit ci sync --init only scaffolds files -- these need a human):',
309
+ '',
310
+ '1. Secrets -- add in the repository (or org) Settings > Secrets and variables > Actions:',
311
+ ' - BIT_CONFIG_ACCESS_TOKEN (required): a bit.cloud service-account token with export rights on',
312
+ " this scope. This account must also be exempt from the org's",
313
+ ' change-request/merge block -- it is the only identity that merges lanes.',
314
+ ' - BIT_SYNC_GH_TOKEN (optional): a GitHub PAT or App token. Without it, sync pushes/PRs use the',
315
+ ' default GITHUB_TOKEN, which does NOT trigger downstream workflow runs (loop-safe, but the',
316
+ ' sync PR then gets no CI checks).',
317
+ '',
318
+ '2. Permissions -- both scaffolded workflows already declare `contents: write` and',
319
+ ' `pull-requests: write`. Nothing to change unless an org-wide policy overrides workflow-level',
320
+ ' permissions (e.g. a default of "Read repository contents permission" for the GITHUB_TOKEN).',
321
+ '',
322
+ '3. bit.cloud webhook -- create ONE webhook (org Settings > Webhooks) on the "Components > Export',
323
+ ' succeeded" event:',
324
+ ` URL: ${dispatchUrl}`,
325
+ ' Headers: Authorization: Bearer <PAT>',
326
+ ' Accept: application/vnd.github+json',
327
+ ' Custom payload template:',
328
+ ` ${WEBHOOK_PAYLOAD_TEMPLATE}`,
329
+ ' WARNING: create this webhook fresh -- editing an existing webhook drops its custom headers',
330
+ ' (known bit.cloud platform bug, verified 2026-07-29). If the URL, headers or template need to',
331
+ ' change later, delete the webhook and create a new one rather than editing it.',
332
+ '',
333
+ '4. Checkout depth -- both scaffolded workflows already check out with `fetch-depth: 0`. This is',
334
+ ' required: the reconciler reads full git history, and a shallow (--depth=1) clone fails sync',
335
+ ' safely but uselessly (HALTED, no writes). Keep `fetch-depth: 0` if you customize the workflow.',
336
+ ' Narrowing the REFSPEC is fine, though -- a single-branch clone is supported, because every',
337
+ ' fetch sync performs names +refs/heads/*:refs/remotes/origin/* explicitly.',
338
+ '',
339
+ ].join('\n');
340
+ }