gitxplain 0.1.3 → 0.1.8

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,721 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ readdirSync,
6
+ writeFileSync
7
+ } from "node:fs";
8
+ import path from "node:path";
9
+
10
+ const WORKFLOW_DIR = ".github/workflows";
11
+
12
+ function readJsonFile(filePath) {
13
+ return JSON.parse(readFileSync(filePath, "utf8"));
14
+ }
15
+
16
+ function fileExists(cwd, relativePath) {
17
+ return existsSync(path.join(cwd, relativePath));
18
+ }
19
+
20
+ function detectPackageManager(cwd) {
21
+ if (fileExists(cwd, "pnpm-lock.yaml")) {
22
+ return "pnpm";
23
+ }
24
+
25
+ if (fileExists(cwd, "yarn.lock")) {
26
+ return "yarn";
27
+ }
28
+
29
+ if (fileExists(cwd, "package-lock.json")) {
30
+ return "npm";
31
+ }
32
+
33
+ return "npm";
34
+ }
35
+
36
+ function normalizeNodeVersion(raw) {
37
+ if (!raw) {
38
+ return null;
39
+ }
40
+
41
+ const match = raw.trim().match(/\d+(?:\.\d+){0,2}/);
42
+ return match ? match[0] : null;
43
+ }
44
+
45
+ function detectNodeVersion(cwd, packageJson) {
46
+ if (fileExists(cwd, ".nvmrc")) {
47
+ return {
48
+ source: "file",
49
+ value: ".nvmrc"
50
+ };
51
+ }
52
+
53
+ const engineVersion = normalizeNodeVersion(packageJson?.engines?.node);
54
+ if (engineVersion) {
55
+ return {
56
+ source: "value",
57
+ value: engineVersion
58
+ };
59
+ }
60
+
61
+ return {
62
+ source: "value",
63
+ value: "20"
64
+ };
65
+ }
66
+
67
+ function detectNodeProject(cwd) {
68
+ if (!fileExists(cwd, "package.json")) {
69
+ return null;
70
+ }
71
+
72
+ const packageJson = readJsonFile(path.join(cwd, "package.json"));
73
+ const scripts = packageJson.scripts ?? {};
74
+ const nodeVersion = detectNodeVersion(cwd, packageJson);
75
+ const packageManager = detectPackageManager(cwd);
76
+ const releaseSupported = packageJson.private !== true && typeof packageJson.name === "string";
77
+ const packSupported = packageJson.private !== true || Boolean(packageJson.bin);
78
+
79
+ return {
80
+ type: "node",
81
+ displayName: packageJson.name || path.basename(cwd),
82
+ packageManager,
83
+ packageJson,
84
+ nodeVersion,
85
+ commands: {
86
+ install:
87
+ packageManager === "pnpm"
88
+ ? "pnpm install --frozen-lockfile"
89
+ : packageManager === "yarn"
90
+ ? "yarn install --frozen-lockfile"
91
+ : "npm ci",
92
+ lint: typeof scripts.lint === "string" ? `${packageManager} run lint` : null,
93
+ test: typeof scripts.test === "string" ? `${packageManager} test` : null,
94
+ build: typeof scripts.build === "string" ? `${packageManager} run build` : null,
95
+ pack: packSupported ? "npm pack --dry-run" : null
96
+ },
97
+ release: {
98
+ supported: releaseSupported,
99
+ type: "npm",
100
+ packageName: packageJson.name || null
101
+ }
102
+ };
103
+ }
104
+
105
+ function detectPythonProject(cwd) {
106
+ if (!fileExists(cwd, "pyproject.toml") && !fileExists(cwd, "requirements.txt")) {
107
+ return null;
108
+ }
109
+
110
+ return {
111
+ type: "python",
112
+ displayName: path.basename(cwd),
113
+ commands: {
114
+ install: fileExists(cwd, "requirements.txt")
115
+ ? "python -m pip install -r requirements.txt"
116
+ : "python -m pip install -e .",
117
+ lint: null,
118
+ test: fileExists(cwd, "pytest.ini") || fileExists(cwd, "tests") ? "pytest" : null,
119
+ build: "python -m build",
120
+ pack: null
121
+ },
122
+ release: {
123
+ supported: fileExists(cwd, "pyproject.toml"),
124
+ type: "pypi",
125
+ packageName: null
126
+ }
127
+ };
128
+ }
129
+
130
+ function detectGoProject(cwd) {
131
+ if (!fileExists(cwd, "go.mod")) {
132
+ return null;
133
+ }
134
+
135
+ return {
136
+ type: "go",
137
+ displayName: path.basename(cwd),
138
+ commands: {
139
+ install: "go mod download",
140
+ lint: null,
141
+ test: "go test ./...",
142
+ build: "go build ./...",
143
+ pack: null
144
+ },
145
+ release: {
146
+ supported: false,
147
+ type: null,
148
+ packageName: null
149
+ }
150
+ };
151
+ }
152
+
153
+ function detectRustProject(cwd) {
154
+ if (!fileExists(cwd, "Cargo.toml")) {
155
+ return null;
156
+ }
157
+
158
+ return {
159
+ type: "rust",
160
+ displayName: path.basename(cwd),
161
+ commands: {
162
+ install: "cargo fetch",
163
+ lint: "cargo fmt --check\ncargo clippy --all-targets --all-features -- -D warnings",
164
+ test: "cargo test --all-features",
165
+ build: "cargo build --release",
166
+ pack: null
167
+ },
168
+ release: {
169
+ supported: true,
170
+ type: "crates",
171
+ packageName: null
172
+ }
173
+ };
174
+ }
175
+
176
+ function detectGradleProject(cwd) {
177
+ const hasGradleWrapper = fileExists(cwd, "gradlew");
178
+ const settingsFiles = ["settings.gradle.kts", "settings.gradle"];
179
+ const rootBuildFiles = ["build.gradle.kts", "build.gradle"];
180
+ const appBuildFiles = [
181
+ "app/build.gradle.kts",
182
+ "app/build.gradle",
183
+ "android/app/build.gradle.kts",
184
+ "android/app/build.gradle"
185
+ ];
186
+
187
+ const hasSettings = settingsFiles.some((filePath) => fileExists(cwd, filePath));
188
+ const hasRootBuild = rootBuildFiles.some((filePath) => fileExists(cwd, filePath));
189
+
190
+ if (!hasGradleWrapper && !hasSettings && !hasRootBuild) {
191
+ return null;
192
+ }
193
+
194
+ const appBuildPath = appBuildFiles.find((filePath) => fileExists(cwd, filePath)) ?? null;
195
+ const appBuildContent = appBuildPath ? readFileSync(path.join(cwd, appBuildPath), "utf8") : "";
196
+ const isAndroidApp =
197
+ appBuildContent.includes("com.android.application") ||
198
+ appBuildContent.includes("libs.plugins.android.application") ||
199
+ appBuildContent.includes("android {");
200
+
201
+ const gradleCommand = hasGradleWrapper ? "./gradlew" : "gradle";
202
+ const displayName = path.basename(cwd);
203
+
204
+ if (isAndroidApp) {
205
+ const appModule = appBuildPath?.startsWith("android/") ? ":android:app" : ":app";
206
+
207
+ return {
208
+ type: "gradle-android",
209
+ displayName,
210
+ commands: {
211
+ install: null,
212
+ lint: `${gradleCommand} ${appModule}:lintDebug`,
213
+ test: `${gradleCommand} ${appModule}:testDebugUnitTest`,
214
+ build: `${gradleCommand} ${appModule}:assembleDebug`,
215
+ pack: null
216
+ },
217
+ release: {
218
+ supported: false,
219
+ type: null,
220
+ packageName: null
221
+ }
222
+ };
223
+ }
224
+
225
+ return {
226
+ type: "gradle",
227
+ displayName,
228
+ commands: {
229
+ install: null,
230
+ lint: null,
231
+ test: `${gradleCommand} test`,
232
+ build: `${gradleCommand} build`,
233
+ pack: null
234
+ },
235
+ release: {
236
+ supported: false,
237
+ type: null,
238
+ packageName: null
239
+ }
240
+ };
241
+ }
242
+
243
+ function detectDockerSupport(cwd) {
244
+ return fileExists(cwd, "Dockerfile");
245
+ }
246
+
247
+ function listExistingWorkflows(cwd) {
248
+ const workflowDir = path.join(cwd, WORKFLOW_DIR);
249
+ if (!existsSync(workflowDir)) {
250
+ return [];
251
+ }
252
+
253
+ return readdirSync(workflowDir).filter((entry) => entry.endsWith(".yml") || entry.endsWith(".yaml"));
254
+ }
255
+
256
+ function formatRunStep(name, command, extraLines = []) {
257
+ const lines = [` - name: ${name}`];
258
+
259
+ if (extraLines.length > 0) {
260
+ lines.push(...extraLines);
261
+ }
262
+
263
+ if (command.includes("\n")) {
264
+ lines.push(" run: |");
265
+ lines.push(...command.split("\n").map((line) => ` ${line}`));
266
+ } else {
267
+ lines.push(` run: ${command}`);
268
+ }
269
+
270
+ return lines.join("\n");
271
+ }
272
+
273
+ function buildNodeSetupStep(nodeVersion, packageManager = "npm") {
274
+ if (nodeVersion.source === "file") {
275
+ return [
276
+ " - name: Setup Node.js",
277
+ " uses: actions/setup-node@v4",
278
+ " with:",
279
+ " node-version-file: .nvmrc",
280
+ ` cache: ${packageManager}`
281
+ ].join("\n");
282
+ }
283
+
284
+ return [
285
+ " - name: Setup Node.js",
286
+ " uses: actions/setup-node@v4",
287
+ " with:",
288
+ ` node-version: '${nodeVersion.value}'`,
289
+ ` cache: ${packageManager}`
290
+ ].join("\n");
291
+ }
292
+
293
+ function buildInstallStep(context) {
294
+ if (context.type === "node" && (context.packageManager === "pnpm" || context.packageManager === "yarn")) {
295
+ return [
296
+ " - name: Enable Corepack",
297
+ " run: corepack enable",
298
+ " - name: Install dependencies",
299
+ ` run: ${context.commands.install}`
300
+ ].join("\n");
301
+ }
302
+
303
+ if (context.type === "python") {
304
+ return [
305
+ " - name: Setup Python",
306
+ " uses: actions/setup-python@v5",
307
+ " with:",
308
+ " python-version: '3.12'",
309
+ " - name: Install dependencies",
310
+ ` run: ${context.commands.install}`
311
+ ].join("\n");
312
+ }
313
+
314
+ if (context.type === "go") {
315
+ return [
316
+ " - name: Setup Go",
317
+ " uses: actions/setup-go@v5",
318
+ " with:",
319
+ " go-version: '1.22'",
320
+ " - name: Download modules",
321
+ ` run: ${context.commands.install}`
322
+ ].join("\n");
323
+ }
324
+
325
+ if (context.type === "rust") {
326
+ return [
327
+ " - name: Install Rust toolchain",
328
+ " uses: dtolnay/rust-toolchain@stable",
329
+ formatRunStep("Fetch dependencies", context.commands.install)
330
+ ].join("\n");
331
+ }
332
+
333
+ if (context.type === "gradle" || context.type === "gradle-android") {
334
+ return [
335
+ " - name: Setup Java",
336
+ " uses: actions/setup-java@v4",
337
+ " with:",
338
+ " distribution: temurin",
339
+ " java-version: '17'",
340
+ " - name: Setup Gradle",
341
+ " uses: gradle/actions/setup-gradle@v4",
342
+ " - name: Make Gradle wrapper executable",
343
+ " run: chmod +x gradlew"
344
+ ].join("\n");
345
+ }
346
+
347
+ return formatRunStep("Install dependencies", context.commands.install);
348
+ }
349
+
350
+ function buildNodeReleaseSetup(context) {
351
+ const lines = [];
352
+
353
+ lines.push(buildNodeSetupStep(context.nodeVersion, context.packageManager));
354
+
355
+ if (context.packageManager === "pnpm" || context.packageManager === "yarn") {
356
+ lines.push(" - name: Enable Corepack");
357
+ lines.push(" run: corepack enable");
358
+ }
359
+
360
+ lines.push(formatRunStep("Install dependencies", context.commands.install));
361
+
362
+ return lines.join("\n");
363
+ }
364
+
365
+ function buildRunSteps(context) {
366
+ const steps = [];
367
+
368
+ if (context.commands.lint) {
369
+ steps.push(formatRunStep("Lint", context.commands.lint));
370
+ }
371
+
372
+ if (context.commands.test) {
373
+ steps.push(formatRunStep("Test", context.commands.test));
374
+ }
375
+
376
+ if (context.commands.build) {
377
+ steps.push(formatRunStep("Build", context.commands.build));
378
+ }
379
+
380
+ if (context.commands.pack) {
381
+ const extraLines =
382
+ context.type === "node"
383
+ ? [
384
+ " env:",
385
+ " npm_config_cache: ${{ runner.temp }}/npm-cache"
386
+ ]
387
+ : [];
388
+ steps.push(formatRunStep("Verify package", context.commands.pack, extraLines));
389
+ }
390
+
391
+ return steps.join("\n");
392
+ }
393
+
394
+ export function inspectRepositoryForPipeline(cwd) {
395
+ const node = detectNodeProject(cwd);
396
+ const python = detectPythonProject(cwd);
397
+ const go = detectGoProject(cwd);
398
+ const rust = detectRustProject(cwd);
399
+ const gradle = detectGradleProject(cwd);
400
+ const primary = node ?? python ?? go ?? rust ?? gradle;
401
+
402
+ if (!primary) {
403
+ return {
404
+ supported: false,
405
+ reason: "No supported Node, Python, Go, Rust, or Gradle project files were detected.",
406
+ existingWorkflows: listExistingWorkflows(cwd),
407
+ options: []
408
+ };
409
+ }
410
+
411
+ const options = [
412
+ {
413
+ id: "ci",
414
+ label: "GitHub Actions CI verification",
415
+ description: "Runs install, lint, test, build, and package checks when supported.",
416
+ files: [".github/workflows/ci.yml"]
417
+ }
418
+ ];
419
+
420
+ if (primary.release.supported) {
421
+ options.push({
422
+ id: "ci-release",
423
+ label: `CI plus ${primary.release.type} release automation`,
424
+ description:
425
+ primary.type === "node"
426
+ ? "Publishes to npm when you push a version tag like v1.2.3."
427
+ : primary.type === "python"
428
+ ? "Builds and publishes to PyPI when you push a version tag like v1.2.3."
429
+ : "Publishes to crates.io when you push a version tag like v1.2.3.",
430
+ files: [".github/workflows/ci.yml", ".github/workflows/release.yml"]
431
+ });
432
+ }
433
+
434
+ if (detectDockerSupport(cwd)) {
435
+ options.push({
436
+ id: "container",
437
+ label: "Container build and GHCR publish",
438
+ description: "Builds the Docker image in CI and publishes it to GitHub Container Registry on tags.",
439
+ files: [".github/workflows/container.yml"]
440
+ });
441
+ }
442
+
443
+ return {
444
+ supported: true,
445
+ primary,
446
+ existingWorkflows: listExistingWorkflows(cwd),
447
+ options
448
+ };
449
+ }
450
+
451
+ export function formatPipelineRecommendations(analysis) {
452
+ if (!analysis.supported) {
453
+ return analysis.reason;
454
+ }
455
+
456
+ const lines = [
457
+ `Detected project type: ${analysis.primary.type}`,
458
+ `Project: ${analysis.primary.displayName}`
459
+ ];
460
+
461
+ if (analysis.primary.type === "node") {
462
+ lines.push(`Package manager: ${analysis.primary.packageManager}`);
463
+ if (analysis.primary.commands.lint) {
464
+ lines.push(`Lint command: ${analysis.primary.commands.lint}`);
465
+ }
466
+ if (analysis.primary.commands.test) {
467
+ lines.push(`Test command: ${analysis.primary.commands.test}`);
468
+ }
469
+ if (analysis.primary.commands.build) {
470
+ lines.push(`Build command: ${analysis.primary.commands.build}`);
471
+ }
472
+ } else {
473
+ if (analysis.primary.commands.lint) {
474
+ lines.push(`Lint command: ${analysis.primary.commands.lint}`);
475
+ }
476
+ if (analysis.primary.commands.test) {
477
+ lines.push(`Test command: ${analysis.primary.commands.test}`);
478
+ }
479
+ if (analysis.primary.commands.build) {
480
+ lines.push(`Build command: ${analysis.primary.commands.build}`);
481
+ }
482
+ }
483
+
484
+ if (analysis.existingWorkflows.length > 0) {
485
+ lines.push(`Existing workflows: ${analysis.existingWorkflows.join(", ")}`);
486
+ } else {
487
+ lines.push("Existing workflows: none");
488
+ }
489
+
490
+ lines.push("");
491
+ lines.push("Available pipeline options:");
492
+
493
+ analysis.options.forEach((option, index) => {
494
+ lines.push(`${index + 1}. ${option.label}`);
495
+ lines.push(` ${option.description}`);
496
+ lines.push(` Files: ${option.files.join(", ")}`);
497
+ });
498
+
499
+ return lines.join("\n");
500
+ }
501
+
502
+ export function resolvePipelineSelection(analysis, response) {
503
+ const normalized = response.trim().toLowerCase();
504
+
505
+ if (normalized === "cancel" || normalized === "q" || normalized === "quit") {
506
+ return null;
507
+ }
508
+
509
+ const numeric = Number.parseInt(normalized, 10);
510
+ if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= analysis.options.length) {
511
+ return analysis.options[numeric - 1];
512
+ }
513
+
514
+ return analysis.options.find((option) => option.id === normalized) ?? null;
515
+ }
516
+
517
+ export function buildCiWorkflow(context) {
518
+ const lines = [
519
+ "name: CI",
520
+ "",
521
+ "on:",
522
+ " push:",
523
+ " branches: [main, master]",
524
+ " pull_request:",
525
+ "",
526
+ "jobs:",
527
+ " verify:",
528
+ " runs-on: ubuntu-latest",
529
+ " steps:",
530
+ " - name: Checkout",
531
+ " uses: actions/checkout@v4"
532
+ ];
533
+
534
+ if (context.type === "node") {
535
+ lines.push(buildNodeSetupStep(context.nodeVersion, context.packageManager));
536
+ }
537
+
538
+ lines.push(buildInstallStep(context));
539
+
540
+ const runSteps = buildRunSteps(context);
541
+ if (runSteps) {
542
+ lines.push(runSteps);
543
+ }
544
+
545
+ return `${lines.join("\n")}\n`;
546
+ }
547
+
548
+ export function buildReleaseWorkflow(context) {
549
+ if (!context.release.supported) {
550
+ throw new Error(`Release automation is not supported for ${context.type} repositories.`);
551
+ }
552
+
553
+ if (context.type === "node") {
554
+ return [
555
+ "name: Release",
556
+ "",
557
+ "on:",
558
+ " push:",
559
+ " tags:",
560
+ " - 'v*.*.*'",
561
+ "",
562
+ "jobs:",
563
+ " publish:",
564
+ " runs-on: ubuntu-latest",
565
+ " permissions:",
566
+ " contents: read",
567
+ " steps:",
568
+ " - name: Checkout",
569
+ " uses: actions/checkout@v4",
570
+ buildNodeReleaseSetup(context),
571
+ context.commands.test ? formatRunStep("Test", context.commands.test) : "",
572
+ context.commands.build ? formatRunStep("Build", context.commands.build) : "",
573
+ " - name: Publish to npm",
574
+ " env:",
575
+ " NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}",
576
+ " run: npm publish"
577
+ ]
578
+ .filter(Boolean)
579
+ .join("\n")
580
+ .concat("\n");
581
+ }
582
+
583
+ if (context.type === "python") {
584
+ return [
585
+ "name: Release",
586
+ "",
587
+ "on:",
588
+ " push:",
589
+ " tags:",
590
+ " - 'v*.*.*'",
591
+ "",
592
+ "jobs:",
593
+ " publish:",
594
+ " runs-on: ubuntu-latest",
595
+ " steps:",
596
+ " - name: Checkout",
597
+ " uses: actions/checkout@v4",
598
+ " - name: Setup Python",
599
+ " uses: actions/setup-python@v5",
600
+ " with:",
601
+ " python-version: '3.12'",
602
+ " - name: Install build tools",
603
+ " run: python -m pip install build twine",
604
+ " - name: Build package",
605
+ " run: python -m build",
606
+ " - name: Publish to PyPI",
607
+ " env:",
608
+ " TWINE_USERNAME: __token__",
609
+ " TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}",
610
+ " run: python -m twine upload dist/*"
611
+ ].join("\n").concat("\n");
612
+ }
613
+
614
+ return [
615
+ "name: Release",
616
+ "",
617
+ "on:",
618
+ " push:",
619
+ " tags:",
620
+ " - 'v*.*.*'",
621
+ "",
622
+ "jobs:",
623
+ " publish:",
624
+ " runs-on: ubuntu-latest",
625
+ " steps:",
626
+ " - name: Checkout",
627
+ " uses: actions/checkout@v4",
628
+ " - name: Install Rust toolchain",
629
+ " uses: dtolnay/rust-toolchain@stable",
630
+ " - name: Publish to crates.io",
631
+ " env:",
632
+ " CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}",
633
+ " run: cargo publish --locked"
634
+ ].join("\n").concat("\n");
635
+ }
636
+
637
+ export function buildContainerWorkflow() {
638
+ return [
639
+ "name: Container",
640
+ "",
641
+ "on:",
642
+ " push:",
643
+ " branches: [main, master]",
644
+ " tags:",
645
+ " - 'v*.*.*'",
646
+ " pull_request:",
647
+ "",
648
+ "jobs:",
649
+ " docker:",
650
+ " runs-on: ubuntu-latest",
651
+ " permissions:",
652
+ " contents: read",
653
+ " packages: write",
654
+ " steps:",
655
+ " - name: Checkout",
656
+ " uses: actions/checkout@v4",
657
+ " - name: Log in to GHCR",
658
+ " if: github.event_name != 'pull_request'",
659
+ " uses: docker/login-action@v3",
660
+ " with:",
661
+ " registry: ghcr.io",
662
+ " username: ${{ github.actor }}",
663
+ " password: ${{ secrets.GITHUB_TOKEN }}",
664
+ " - name: Extract metadata",
665
+ " id: meta",
666
+ " uses: docker/metadata-action@v5",
667
+ " with:",
668
+ " images: ghcr.io/${{ github.repository }}",
669
+ " - name: Build and push image",
670
+ " uses: docker/build-push-action@v6",
671
+ " with:",
672
+ " context: .",
673
+ " push: ${{ github.event_name != 'pull_request' }}",
674
+ " tags: ${{ steps.meta.outputs.tags }}",
675
+ " labels: ${{ steps.meta.outputs.labels }}"
676
+ ].join("\n").concat("\n");
677
+ }
678
+
679
+ export function writePipelineFiles(cwd, analysis, selection) {
680
+ if (!analysis.supported) {
681
+ throw new Error(analysis.reason);
682
+ }
683
+
684
+ const workflowDir = path.join(cwd, WORKFLOW_DIR);
685
+ mkdirSync(workflowDir, { recursive: true });
686
+
687
+ const writtenFiles = [];
688
+ const notes = [];
689
+
690
+ const writeWorkflow = (relativePath, contents) => {
691
+ const absolutePath = path.join(cwd, relativePath);
692
+ writeFileSync(absolutePath, contents, "utf8");
693
+ writtenFiles.push(relativePath);
694
+ };
695
+
696
+ if (selection.id === "ci" || selection.id === "ci-release") {
697
+ writeWorkflow(".github/workflows/ci.yml", buildCiWorkflow(analysis.primary));
698
+ }
699
+
700
+ if (selection.id === "ci-release") {
701
+ writeWorkflow(".github/workflows/release.yml", buildReleaseWorkflow(analysis.primary));
702
+
703
+ if (analysis.primary.release.type === "npm") {
704
+ notes.push("Add an `NPM_TOKEN` repository secret before pushing a release tag.");
705
+ } else if (analysis.primary.release.type === "pypi") {
706
+ notes.push("Add a `PYPI_TOKEN` repository secret before pushing a release tag.");
707
+ } else if (analysis.primary.release.type === "crates") {
708
+ notes.push("Add a `CARGO_REGISTRY_TOKEN` repository secret before pushing a release tag.");
709
+ }
710
+ }
711
+
712
+ if (selection.id === "container") {
713
+ writeWorkflow(".github/workflows/container.yml", buildContainerWorkflow());
714
+ }
715
+
716
+ if (selection.id === "container" && !selection.files.includes(".github/workflows/ci.yml")) {
717
+ notes.push("This option only creates the container workflow. Run `gitxplain --pipeline` again if you also want CI verification.");
718
+ }
719
+
720
+ return { writtenFiles, notes };
721
+ }