@bensandee/tooling 0.28.0 → 0.29.0
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/dist/bin.mjs +926 -637
- package/dist/docker-check/index.mjs +1 -1
- package/dist/index.d.mts +4 -3
- package/package.json +11 -4
- package/tooling.schema.json +0 -4
- package/dist/{check-D41R218h.mjs → check-DMDdHanG.mjs} +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { l as createRealExecutor$1, t as runDockerCheck, u as isExecSyncError } from "./check-
|
|
2
|
+
import { l as createRealExecutor$1, t as runDockerCheck, u as isExecSyncError } from "./check-DMDdHanG.mjs";
|
|
3
3
|
import { defineCommand, runMain } from "citty";
|
|
4
4
|
import * as clack from "@clack/prompts";
|
|
5
5
|
import { isCancel, select } from "@clack/prompts";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
8
9
|
import JSON5 from "json5";
|
|
9
10
|
import { parse } from "jsonc-parser";
|
|
10
11
|
import { z } from "zod";
|
|
11
12
|
import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
|
|
12
13
|
import { isMap, isScalar, isSeq, parse as parse$1, parseDocument, stringify } from "yaml";
|
|
13
|
-
import { execSync } from "node:child_process";
|
|
14
14
|
import picomatch from "picomatch";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
//#region src/utils/log.ts
|
|
@@ -130,9 +130,43 @@ function detectProject(targetDir) {
|
|
|
130
130
|
hasChangesetsConfig: exists(".changeset/config.json"),
|
|
131
131
|
hasRepositoryField: !!rootPkg?.repository,
|
|
132
132
|
hasCommitAndTagVersion: !!rootPkg?.devDependencies?.["commit-and-tag-version"],
|
|
133
|
+
gitRemoteUrl: detectGitRemoteUrl(targetDir),
|
|
133
134
|
legacyConfigs: detectLegacyConfigs(targetDir)
|
|
134
135
|
};
|
|
135
136
|
}
|
|
137
|
+
/** Read git remote origin URL and normalize to HTTPS. */
|
|
138
|
+
function detectGitRemoteUrl(targetDir) {
|
|
139
|
+
try {
|
|
140
|
+
return normalizeGitUrl(execSync("git remote get-url origin", {
|
|
141
|
+
cwd: targetDir,
|
|
142
|
+
stdio: [
|
|
143
|
+
"ignore",
|
|
144
|
+
"pipe",
|
|
145
|
+
"ignore"
|
|
146
|
+
],
|
|
147
|
+
timeout: 5e3
|
|
148
|
+
}).toString().trim());
|
|
149
|
+
} catch (_error) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Normalize a git remote URL to a clean HTTPS URL.
|
|
155
|
+
* Handles SSH (git@host:owner/repo.git) and HTTPS variants.
|
|
156
|
+
*/
|
|
157
|
+
function normalizeGitUrl(raw) {
|
|
158
|
+
const sshMatch = raw.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
|
159
|
+
if (sshMatch) return `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
160
|
+
try {
|
|
161
|
+
const url = new URL(raw);
|
|
162
|
+
url.pathname = url.pathname.replace(/\.git$/, "");
|
|
163
|
+
url.username = "";
|
|
164
|
+
url.password = "";
|
|
165
|
+
return url.toString().replace(/\/$/, "");
|
|
166
|
+
} catch (_error) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
136
170
|
/** Scan for legacy/conflicting tooling config files. */
|
|
137
171
|
function detectLegacyConfigs(targetDir) {
|
|
138
172
|
let entries;
|
|
@@ -280,14 +314,18 @@ function getMonorepoPackages(targetDir) {
|
|
|
280
314
|
function isCancelled(value) {
|
|
281
315
|
return clack.isCancel(value);
|
|
282
316
|
}
|
|
317
|
+
function detectProjectInfo(targetDir) {
|
|
318
|
+
const existingPkg = readPackageJson(targetDir);
|
|
319
|
+
return {
|
|
320
|
+
detected: detectProject(targetDir),
|
|
321
|
+
defaults: computeDefaults(targetDir),
|
|
322
|
+
name: existingPkg?.name ?? path.basename(targetDir)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
283
325
|
async function runInitPrompts(targetDir, saved) {
|
|
284
326
|
clack.intro("@bensandee/tooling repo:sync");
|
|
285
|
-
const
|
|
286
|
-
const detected = detectProject(targetDir);
|
|
287
|
-
const defaults = computeDefaults(targetDir);
|
|
288
|
-
const isExisting = detected.hasPackageJson;
|
|
327
|
+
const { detected, defaults, name } = detectProjectInfo(targetDir);
|
|
289
328
|
const isFirstInit = !saved;
|
|
290
|
-
const name = existingPkg?.name ?? path.basename(targetDir);
|
|
291
329
|
const structure = saved?.structure ?? defaults.structure;
|
|
292
330
|
const useEslintPlugin = saved?.useEslintPlugin ?? defaults.useEslintPlugin;
|
|
293
331
|
let formatter = saved?.formatter ?? defaults.formatter;
|
|
@@ -406,7 +444,6 @@ async function runInitPrompts(targetDir, saved) {
|
|
|
406
444
|
clack.outro("Configuration complete!");
|
|
407
445
|
return {
|
|
408
446
|
name,
|
|
409
|
-
isNew: !isExisting,
|
|
410
447
|
structure,
|
|
411
448
|
useEslintPlugin,
|
|
412
449
|
formatter,
|
|
@@ -423,12 +460,9 @@ async function runInitPrompts(targetDir, saved) {
|
|
|
423
460
|
}
|
|
424
461
|
/** Build a ProjectConfig from CLI flags for non-interactive mode. */
|
|
425
462
|
function buildDefaultConfig(targetDir, flags) {
|
|
426
|
-
const
|
|
427
|
-
const detected = detectProject(targetDir);
|
|
428
|
-
const defaults = computeDefaults(targetDir);
|
|
463
|
+
const { defaults, name } = detectProjectInfo(targetDir);
|
|
429
464
|
return {
|
|
430
|
-
name
|
|
431
|
-
isNew: !detected.hasPackageJson,
|
|
465
|
+
name,
|
|
432
466
|
...defaults,
|
|
433
467
|
...flags.eslintPlugin !== void 0 && { useEslintPlugin: flags.eslintPlugin },
|
|
434
468
|
...flags.noCi && { ci: "none" },
|
|
@@ -574,7 +608,6 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
574
608
|
"library"
|
|
575
609
|
]).optional().meta({ description: "Project type (determines tsconfig base)" }),
|
|
576
610
|
detectPackageTypes: z.boolean().optional().meta({ description: "Auto-detect project types for monorepo packages" }),
|
|
577
|
-
setupDocker: z.boolean().optional().meta({ description: "Generate Docker build/check scripts" }),
|
|
578
611
|
docker: z.record(z.string(), z.object({
|
|
579
612
|
dockerfile: z.string().meta({ description: "Path to Dockerfile relative to package" }),
|
|
580
613
|
context: z.string().default(".").meta({ description: "Docker build context relative to package" })
|
|
@@ -637,7 +670,6 @@ function saveToolingConfig(ctx, config) {
|
|
|
637
670
|
function mergeWithSavedConfig(detected, saved) {
|
|
638
671
|
return {
|
|
639
672
|
name: detected.name,
|
|
640
|
-
isNew: detected.isNew,
|
|
641
673
|
targetDir: detected.targetDir,
|
|
642
674
|
structure: saved.structure ?? detected.structure,
|
|
643
675
|
useEslintPlugin: saved.useEslintPlugin ?? detected.useEslintPlugin,
|
|
@@ -653,173 +685,499 @@ function mergeWithSavedConfig(detected, saved) {
|
|
|
653
685
|
};
|
|
654
686
|
}
|
|
655
687
|
//#endregion
|
|
656
|
-
//#region src/
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
688
|
+
//#region src/release/docker.ts
|
|
689
|
+
const ToolingDockerMapSchema = z.record(z.string(), z.object({
|
|
690
|
+
dockerfile: z.string(),
|
|
691
|
+
context: z.string().default(".")
|
|
692
|
+
}));
|
|
693
|
+
const ToolingConfigDockerSchema = z.object({ docker: ToolingDockerMapSchema.optional() });
|
|
694
|
+
const PackageInfoSchema = z.object({
|
|
695
|
+
name: z.string().optional(),
|
|
696
|
+
version: z.string().optional()
|
|
697
|
+
});
|
|
698
|
+
/** Read the docker map from .tooling.json. Returns empty record if missing or invalid. */
|
|
699
|
+
function loadDockerMap(executor, cwd) {
|
|
700
|
+
const configPath = path.join(cwd, ".tooling.json");
|
|
701
|
+
const raw = executor.readFile(configPath);
|
|
702
|
+
if (!raw) return {};
|
|
703
|
+
try {
|
|
704
|
+
const result = ToolingConfigDockerSchema.safeParse(JSON.parse(raw));
|
|
705
|
+
if (!result.success || !result.data.docker) return {};
|
|
706
|
+
return result.data.docker;
|
|
707
|
+
} catch (_error) {
|
|
708
|
+
return {};
|
|
709
|
+
}
|
|
676
710
|
}
|
|
677
|
-
/**
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
if (isToolingIgnored(existing)) return {
|
|
684
|
-
content: existing,
|
|
685
|
-
changed: false
|
|
711
|
+
/** Read name and version from a package's package.json. */
|
|
712
|
+
function readPackageInfo(executor, packageJsonPath) {
|
|
713
|
+
const raw = executor.readFile(packageJsonPath);
|
|
714
|
+
if (!raw) return {
|
|
715
|
+
name: void 0,
|
|
716
|
+
version: void 0
|
|
686
717
|
};
|
|
687
718
|
try {
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
return {
|
|
693
|
-
content: doc.toString(),
|
|
694
|
-
changed: true
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
const commands = doc.getIn(["pre-commit", "commands"]);
|
|
698
|
-
if (!isMap(commands)) return {
|
|
699
|
-
content: existing,
|
|
700
|
-
changed: false
|
|
719
|
+
const result = PackageInfoSchema.safeParse(JSON.parse(raw));
|
|
720
|
+
if (!result.success) return {
|
|
721
|
+
name: void 0,
|
|
722
|
+
version: void 0
|
|
701
723
|
};
|
|
702
|
-
for (const [name, config] of Object.entries(requiredCommands)) if (!commands.has(name)) {
|
|
703
|
-
commands.set(name, config);
|
|
704
|
-
changed = true;
|
|
705
|
-
}
|
|
706
724
|
return {
|
|
707
|
-
|
|
708
|
-
|
|
725
|
+
name: result.data.name,
|
|
726
|
+
version: result.data.version
|
|
709
727
|
};
|
|
710
|
-
} catch {
|
|
728
|
+
} catch (_error) {
|
|
711
729
|
return {
|
|
712
|
-
|
|
713
|
-
|
|
730
|
+
name: void 0,
|
|
731
|
+
version: void 0
|
|
714
732
|
};
|
|
715
733
|
}
|
|
716
734
|
}
|
|
735
|
+
/** Strip npm scope from a package name: "@scope/foo" → "foo", "foo" → "foo". */
|
|
736
|
+
function stripScope(name) {
|
|
737
|
+
const slashIndex = name.indexOf("/");
|
|
738
|
+
return name.startsWith("@") && slashIndex !== -1 ? name.slice(slashIndex + 1) : name;
|
|
739
|
+
}
|
|
740
|
+
/** Convention paths to check for Dockerfiles in a package directory. */
|
|
741
|
+
const CONVENTION_DOCKERFILE_PATHS = ["Dockerfile", "docker/Dockerfile"];
|
|
717
742
|
/**
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
743
|
+
* Find a Dockerfile at convention paths for a monorepo package.
|
|
744
|
+
* Checks packages/{dir}/Dockerfile and packages/{dir}/docker/Dockerfile.
|
|
721
745
|
*/
|
|
722
|
-
function
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const doc = parseDocument(existing);
|
|
729
|
-
const steps = doc.getIn([
|
|
730
|
-
"jobs",
|
|
731
|
-
jobName,
|
|
732
|
-
"steps"
|
|
733
|
-
]);
|
|
734
|
-
if (!isSeq(steps)) return {
|
|
735
|
-
content: existing,
|
|
736
|
-
changed: false
|
|
737
|
-
};
|
|
738
|
-
let changed = false;
|
|
739
|
-
for (const { match, step } of requiredSteps) if (!steps.items.some((item) => {
|
|
740
|
-
if (!isMap(item)) return false;
|
|
741
|
-
if (match.run) {
|
|
742
|
-
const run = item.get("run");
|
|
743
|
-
return typeof run === "string" && run.includes(match.run);
|
|
744
|
-
}
|
|
745
|
-
if (match.uses) {
|
|
746
|
-
const uses = item.get("uses");
|
|
747
|
-
return typeof uses === "string" && uses.startsWith(match.uses);
|
|
748
|
-
}
|
|
749
|
-
return false;
|
|
750
|
-
})) {
|
|
751
|
-
steps.add(doc.createNode(step));
|
|
752
|
-
changed = true;
|
|
753
|
-
}
|
|
754
|
-
return {
|
|
755
|
-
content: changed ? doc.toString() : existing,
|
|
756
|
-
changed
|
|
757
|
-
};
|
|
758
|
-
} catch {
|
|
759
|
-
return {
|
|
760
|
-
content: existing,
|
|
761
|
-
changed: false
|
|
746
|
+
function findConventionDockerfile(executor, cwd, dir) {
|
|
747
|
+
for (const rel of CONVENTION_DOCKERFILE_PATHS) {
|
|
748
|
+
const dockerfilePath = `packages/${dir}/${rel}`;
|
|
749
|
+
if (executor.readFile(path.join(cwd, dockerfilePath)) !== null) return {
|
|
750
|
+
dockerfile: dockerfilePath,
|
|
751
|
+
context: "."
|
|
762
752
|
};
|
|
763
753
|
}
|
|
764
754
|
}
|
|
765
755
|
/**
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
* or the document can't be parsed.
|
|
756
|
+
* Find a Dockerfile at convention paths for a single-package repo.
|
|
757
|
+
* Checks Dockerfile and docker/Dockerfile at the project root.
|
|
769
758
|
*/
|
|
759
|
+
function findRootDockerfile(executor, cwd) {
|
|
760
|
+
for (const rel of CONVENTION_DOCKERFILE_PATHS) if (executor.readFile(path.join(cwd, rel)) !== null) return {
|
|
761
|
+
dockerfile: rel,
|
|
762
|
+
context: "."
|
|
763
|
+
};
|
|
764
|
+
}
|
|
770
765
|
/**
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
*
|
|
766
|
+
* Discover Docker packages by convention and merge with .tooling.json overrides.
|
|
767
|
+
*
|
|
768
|
+
* Convention: any package with a Dockerfile or docker/Dockerfile is a Docker package.
|
|
769
|
+
* For monorepos, scans packages/{name}/. For single-package repos, scans the root.
|
|
770
|
+
* The docker map in .tooling.json overrides convention-discovered config and can add
|
|
771
|
+
* packages at non-standard locations.
|
|
772
|
+
*
|
|
773
|
+
* Image names are derived from {root-name}-{package-name} using each package's package.json name.
|
|
774
|
+
* Versions are read from each package's own package.json.
|
|
774
775
|
*/
|
|
775
|
-
function
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
776
|
+
function detectDockerPackages(executor, cwd, repoName) {
|
|
777
|
+
const overrides = loadDockerMap(executor, cwd);
|
|
778
|
+
const packageDirs = executor.listPackageDirs(cwd);
|
|
779
|
+
const packages = [];
|
|
780
|
+
const seen = /* @__PURE__ */ new Set();
|
|
781
|
+
if (packageDirs.length > 0) {
|
|
782
|
+
for (const dir of packageDirs) {
|
|
783
|
+
const convention = findConventionDockerfile(executor, cwd, dir);
|
|
784
|
+
const docker = overrides[dir] ?? convention;
|
|
785
|
+
if (docker) {
|
|
786
|
+
const { name, version } = readPackageInfo(executor, path.join(cwd, "packages", dir, "package.json"));
|
|
787
|
+
packages.push({
|
|
788
|
+
dir,
|
|
789
|
+
imageName: `${repoName}-${stripScope(name ?? dir)}`,
|
|
790
|
+
version,
|
|
791
|
+
docker
|
|
792
|
+
});
|
|
793
|
+
seen.add(dir);
|
|
794
|
+
}
|
|
793
795
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
796
|
+
for (const [dir, docker] of Object.entries(overrides)) if (!seen.has(dir)) {
|
|
797
|
+
const { name, version } = readPackageInfo(executor, path.join(cwd, "packages", dir, "package.json"));
|
|
798
|
+
packages.push({
|
|
799
|
+
dir,
|
|
800
|
+
imageName: `${repoName}-${stripScope(name ?? dir)}`,
|
|
801
|
+
version,
|
|
802
|
+
docker
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
const convention = findRootDockerfile(executor, cwd);
|
|
807
|
+
const docker = overrides["."] ?? convention;
|
|
808
|
+
if (docker) {
|
|
809
|
+
const { name, version } = readPackageInfo(executor, path.join(cwd, "package.json"));
|
|
810
|
+
packages.push({
|
|
811
|
+
dir: ".",
|
|
812
|
+
imageName: stripScope(name ?? repoName),
|
|
813
|
+
version,
|
|
814
|
+
docker
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return packages;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Read docker config for a single package, checking convention paths first,
|
|
822
|
+
* then .tooling.json overrides. Used by the per-package image:build script.
|
|
823
|
+
*/
|
|
824
|
+
function readSinglePackageDocker(executor, cwd, packageDir, repoName) {
|
|
825
|
+
const dir = path.basename(path.resolve(cwd, packageDir));
|
|
826
|
+
const convention = findConventionDockerfile(executor, cwd, dir);
|
|
827
|
+
const docker = loadDockerMap(executor, cwd)[dir] ?? convention;
|
|
828
|
+
if (!docker) throw new FatalError(`No Dockerfile found for package "${dir}" (checked convention paths and .tooling.json)`);
|
|
829
|
+
const { name, version } = readPackageInfo(executor, path.join(cwd, "packages", dir, "package.json"));
|
|
830
|
+
return {
|
|
831
|
+
dir,
|
|
832
|
+
imageName: `${repoName}-${stripScope(name ?? dir)}`,
|
|
833
|
+
version,
|
|
834
|
+
docker
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
/** Parse semver version string into major, minor, patch components. */
|
|
838
|
+
function parseSemver(version) {
|
|
839
|
+
const clean = version.replace(/^v/, "");
|
|
840
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(clean);
|
|
841
|
+
if (!match?.[1] || !match[2] || !match[3]) throw new FatalError(`Invalid semver version: ${version}`);
|
|
842
|
+
return {
|
|
843
|
+
major: Number(match[1]),
|
|
844
|
+
minor: Number(match[2]),
|
|
845
|
+
patch: Number(match[3])
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
/** Generate semver tag variants: latest, vX.Y.Z, vX.Y, vX */
|
|
849
|
+
function generateTags(version) {
|
|
850
|
+
const { major, minor, patch } = parseSemver(version);
|
|
851
|
+
return [
|
|
852
|
+
"latest",
|
|
853
|
+
`v${major}.${minor}.${patch}`,
|
|
854
|
+
`v${major}.${minor}`,
|
|
855
|
+
`v${major}`
|
|
856
|
+
];
|
|
857
|
+
}
|
|
858
|
+
/** Build the full image reference: namespace/imageName:tag */
|
|
859
|
+
function imageRef(namespace, imageName, tag) {
|
|
860
|
+
return `${namespace}/${imageName}:${tag}`;
|
|
861
|
+
}
|
|
862
|
+
function log$1(message) {
|
|
863
|
+
console.log(message);
|
|
864
|
+
}
|
|
865
|
+
/** Read the repo name from root package.json. */
|
|
866
|
+
function readRepoName(executor, cwd) {
|
|
867
|
+
const rootPkgRaw = executor.readFile(path.join(cwd, "package.json"));
|
|
868
|
+
if (!rootPkgRaw) throw new FatalError("No package.json found in project root");
|
|
869
|
+
const repoName = parsePackageJson(rootPkgRaw)?.name;
|
|
870
|
+
if (!repoName) throw new FatalError("Root package.json must have a name field");
|
|
871
|
+
return repoName;
|
|
872
|
+
}
|
|
873
|
+
/** Build a single docker image from its config. Paths are resolved relative to cwd. */
|
|
874
|
+
function buildImage(executor, pkg, cwd, extraArgs) {
|
|
875
|
+
const dockerfilePath = path.resolve(cwd, pkg.docker.dockerfile);
|
|
876
|
+
const contextPath = path.resolve(cwd, pkg.docker.context);
|
|
877
|
+
const command = [
|
|
878
|
+
"docker build",
|
|
879
|
+
`-f ${dockerfilePath}`,
|
|
880
|
+
`-t ${pkg.imageName}:latest`,
|
|
881
|
+
...extraArgs,
|
|
882
|
+
contextPath
|
|
883
|
+
].join(" ");
|
|
884
|
+
executor.execInherit(command);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Detect packages with docker config in .tooling.json and build each one.
|
|
888
|
+
* Runs `docker build -f <dockerfile> -t <image-name>:latest <context>` for each package.
|
|
889
|
+
* Dockerfile and context paths are resolved relative to the project root.
|
|
890
|
+
*
|
|
891
|
+
* When `packageDir` is set, builds only that single package (for use as an image:build script).
|
|
892
|
+
*/
|
|
893
|
+
function runDockerBuild(executor, config) {
|
|
894
|
+
const repoName = readRepoName(executor, config.cwd);
|
|
895
|
+
if (config.packageDir) {
|
|
896
|
+
const pkg = readSinglePackageDocker(executor, config.cwd, config.packageDir, repoName);
|
|
897
|
+
log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
898
|
+
buildImage(executor, pkg, config.cwd, config.extraArgs);
|
|
899
|
+
log$1(`Built ${pkg.imageName}:latest`);
|
|
900
|
+
return { packages: [pkg] };
|
|
901
|
+
}
|
|
902
|
+
const packages = detectDockerPackages(executor, config.cwd, repoName);
|
|
903
|
+
if (packages.length === 0) {
|
|
904
|
+
log$1("No packages with docker config found");
|
|
905
|
+
return { packages: [] };
|
|
906
|
+
}
|
|
907
|
+
log$1(`Found ${packages.length} Docker package(s): ${packages.map((p) => p.dir).join(", ")}`);
|
|
908
|
+
for (const pkg of packages) {
|
|
909
|
+
log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
910
|
+
buildImage(executor, pkg, config.cwd, config.extraArgs);
|
|
911
|
+
}
|
|
912
|
+
log$1(`Built ${packages.length} image(s)`);
|
|
913
|
+
return { packages };
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Run the full Docker publish pipeline:
|
|
917
|
+
* 1. Build all images via runDockerBuild
|
|
918
|
+
* 2. Login to registry
|
|
919
|
+
* 3. Tag each image with semver variants from its own package.json version
|
|
920
|
+
* 4. Push all tags
|
|
921
|
+
* 5. Logout from registry
|
|
922
|
+
*/
|
|
923
|
+
function runDockerPublish(executor, config) {
|
|
924
|
+
const { packages } = runDockerBuild(executor, {
|
|
925
|
+
cwd: config.cwd,
|
|
926
|
+
packageDir: void 0,
|
|
927
|
+
extraArgs: []
|
|
928
|
+
});
|
|
929
|
+
if (packages.length === 0) return {
|
|
930
|
+
packages: [],
|
|
931
|
+
tags: []
|
|
932
|
+
};
|
|
933
|
+
for (const pkg of packages) if (!pkg.version) throw new FatalError(`Package ${pkg.dir} has docker config but no version in package.json`);
|
|
934
|
+
if (!config.dryRun) {
|
|
935
|
+
log$1(`Logging in to ${config.registryHost}...`);
|
|
936
|
+
const loginResult = executor.exec(`echo "${config.password}" | docker login ${config.registryHost} -u ${config.username} --password-stdin`);
|
|
937
|
+
if (loginResult.exitCode !== 0) throw new FatalError(`Docker login failed: ${loginResult.stderr}`);
|
|
938
|
+
} else log$1("[dry-run] Skipping docker login");
|
|
939
|
+
const allTags = [];
|
|
940
|
+
try {
|
|
941
|
+
for (const pkg of packages) {
|
|
942
|
+
const tags = generateTags(pkg.version ?? "");
|
|
943
|
+
log$1(`${pkg.dir} v${pkg.version} → tags: ${tags.join(", ")}`);
|
|
944
|
+
for (const tag of tags) {
|
|
945
|
+
const ref = imageRef(config.registryNamespace, pkg.imageName, tag);
|
|
946
|
+
allTags.push(ref);
|
|
947
|
+
log$1(`Tagging ${pkg.imageName} → ${ref}`);
|
|
948
|
+
const tagResult = executor.exec(`docker tag ${pkg.imageName} ${ref}`);
|
|
949
|
+
if (tagResult.exitCode !== 0) throw new FatalError(`docker tag failed: ${tagResult.stderr}`);
|
|
950
|
+
if (!config.dryRun) {
|
|
951
|
+
log$1(`Pushing ${ref}...`);
|
|
952
|
+
const pushResult = executor.exec(`docker push ${ref}`);
|
|
953
|
+
if (pushResult.exitCode !== 0) throw new FatalError(`docker push failed: ${pushResult.stderr}`);
|
|
954
|
+
} else log$1(`[dry-run] Skipping push for ${ref}`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
} finally {
|
|
958
|
+
if (!config.dryRun) {
|
|
959
|
+
log$1(`Logging out from ${config.registryHost}...`);
|
|
960
|
+
executor.exec(`docker logout ${config.registryHost}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
log$1(`Published ${allTags.length} image tag(s)`);
|
|
964
|
+
return {
|
|
965
|
+
packages,
|
|
966
|
+
tags: allTags
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
//#endregion
|
|
970
|
+
//#region src/utils/yaml-merge.ts
|
|
971
|
+
const IGNORE_PATTERN = "@bensandee/tooling:ignore";
|
|
972
|
+
const FORGEJO_SCHEMA_COMMENT = "# yaml-language-server: $schema=../../.vscode/forgejo-workflow.schema.json\n";
|
|
973
|
+
/** Returns a yaml-language-server schema comment for Forgejo workflows, empty string otherwise. */
|
|
974
|
+
function workflowSchemaComment(ci) {
|
|
975
|
+
return ci === "forgejo" ? FORGEJO_SCHEMA_COMMENT : "";
|
|
976
|
+
}
|
|
977
|
+
/** Prepend the Forgejo schema comment if it's not already present. No-op for GitHub. */
|
|
978
|
+
function ensureSchemaComment(content, ci) {
|
|
979
|
+
if (ci !== "forgejo") return content;
|
|
980
|
+
if (content.includes("yaml-language-server")) return content;
|
|
981
|
+
return FORGEJO_SCHEMA_COMMENT + content;
|
|
982
|
+
}
|
|
983
|
+
/** Migrate content from old tooling binary name to new. */
|
|
984
|
+
function migrateToolingBinary(content) {
|
|
985
|
+
return content.replaceAll("pnpm exec tooling ", "pnpm exec bst ");
|
|
986
|
+
}
|
|
987
|
+
/** Check if a YAML file has an opt-out comment in the first 10 lines. */
|
|
988
|
+
function isToolingIgnored(content) {
|
|
989
|
+
return content.split("\n", 10).some((line) => line.includes(IGNORE_PATTERN));
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Ensure required commands exist under `pre-commit.commands` in a lefthook config.
|
|
993
|
+
* Only adds missing commands — never modifies existing ones.
|
|
994
|
+
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
995
|
+
*/
|
|
996
|
+
function mergeLefthookCommands(existing, requiredCommands) {
|
|
997
|
+
if (isToolingIgnored(existing)) return {
|
|
998
|
+
content: existing,
|
|
999
|
+
changed: false
|
|
1000
|
+
};
|
|
1001
|
+
try {
|
|
1002
|
+
const doc = parseDocument(existing);
|
|
1003
|
+
let changed = false;
|
|
1004
|
+
if (!doc.hasIn(["pre-commit", "commands"])) {
|
|
1005
|
+
doc.setIn(["pre-commit", "commands"], requiredCommands);
|
|
1006
|
+
return {
|
|
1007
|
+
content: doc.toString(),
|
|
1008
|
+
changed: true
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
const commands = doc.getIn(["pre-commit", "commands"]);
|
|
1012
|
+
if (!isMap(commands)) return {
|
|
1013
|
+
content: existing,
|
|
1014
|
+
changed: false
|
|
1015
|
+
};
|
|
1016
|
+
for (const [name, config] of Object.entries(requiredCommands)) if (!commands.has(name)) {
|
|
1017
|
+
commands.set(name, config);
|
|
1018
|
+
changed = true;
|
|
1019
|
+
}
|
|
1020
|
+
return {
|
|
1021
|
+
content: changed ? doc.toString() : existing,
|
|
1022
|
+
changed
|
|
1023
|
+
};
|
|
1024
|
+
} catch {
|
|
1025
|
+
return {
|
|
1026
|
+
content: existing,
|
|
1027
|
+
changed: false
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Ensure required steps exist in a workflow job's steps array.
|
|
1033
|
+
* Only adds missing steps at the end — never modifies existing ones.
|
|
1034
|
+
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
1035
|
+
*/
|
|
1036
|
+
function mergeWorkflowSteps(existing, jobName, requiredSteps) {
|
|
1037
|
+
if (isToolingIgnored(existing)) return {
|
|
1038
|
+
content: existing,
|
|
1039
|
+
changed: false
|
|
1040
|
+
};
|
|
1041
|
+
try {
|
|
1042
|
+
const doc = parseDocument(existing);
|
|
1043
|
+
const steps = doc.getIn([
|
|
1044
|
+
"jobs",
|
|
1045
|
+
jobName,
|
|
1046
|
+
"steps"
|
|
1047
|
+
]);
|
|
1048
|
+
if (!isSeq(steps)) return {
|
|
1049
|
+
content: existing,
|
|
1050
|
+
changed: false
|
|
1051
|
+
};
|
|
1052
|
+
let changed = false;
|
|
1053
|
+
for (const { match, step } of requiredSteps) if (!steps.items.some((item) => {
|
|
1054
|
+
if (!isMap(item)) return false;
|
|
1055
|
+
if (match.run) {
|
|
1056
|
+
const run = item.get("run");
|
|
1057
|
+
return typeof run === "string" && run.includes(match.run);
|
|
1058
|
+
}
|
|
1059
|
+
if (match.uses) {
|
|
1060
|
+
const uses = item.get("uses");
|
|
1061
|
+
return typeof uses === "string" && uses.startsWith(match.uses);
|
|
1062
|
+
}
|
|
1063
|
+
return false;
|
|
1064
|
+
})) {
|
|
1065
|
+
steps.add(doc.createNode(step));
|
|
1066
|
+
changed = true;
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
content: changed ? doc.toString() : existing,
|
|
1070
|
+
changed
|
|
1071
|
+
};
|
|
1072
|
+
} catch {
|
|
1073
|
+
return {
|
|
1074
|
+
content: existing,
|
|
1075
|
+
changed: false
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Add a job to an existing workflow YAML if it doesn't already exist.
|
|
1081
|
+
* Returns unchanged content if the job already exists, the file has an opt-out comment,
|
|
1082
|
+
* or the document can't be parsed.
|
|
1083
|
+
*/
|
|
1084
|
+
/**
|
|
1085
|
+
* Ensure a `concurrency` block exists at the workflow top level.
|
|
1086
|
+
* Adds it if missing — never modifies an existing one.
|
|
1087
|
+
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
1088
|
+
*/
|
|
1089
|
+
/**
|
|
1090
|
+
* Ensure `on.push` has `tags-ignore: ["**"]` so tag pushes don't trigger CI.
|
|
1091
|
+
* Only adds the filter when `on.push` exists and `tags-ignore` is absent.
|
|
1092
|
+
*/
|
|
1093
|
+
function ensureWorkflowTagsIgnore(existing) {
|
|
1094
|
+
if (isToolingIgnored(existing)) return {
|
|
1095
|
+
content: existing,
|
|
1096
|
+
changed: false
|
|
1097
|
+
};
|
|
1098
|
+
try {
|
|
1099
|
+
const doc = parseDocument(existing);
|
|
1100
|
+
const on = doc.get("on");
|
|
1101
|
+
if (!isMap(on)) return {
|
|
1102
|
+
content: existing,
|
|
1103
|
+
changed: false
|
|
1104
|
+
};
|
|
1105
|
+
const push = on.get("push");
|
|
1106
|
+
if (!isMap(push)) return {
|
|
1107
|
+
content: existing,
|
|
1108
|
+
changed: false
|
|
1109
|
+
};
|
|
1110
|
+
if (push.has("tags-ignore")) return {
|
|
1111
|
+
content: existing,
|
|
1112
|
+
changed: false
|
|
1113
|
+
};
|
|
1114
|
+
push.set("tags-ignore", ["**"]);
|
|
1115
|
+
return {
|
|
1116
|
+
content: doc.toString(),
|
|
1117
|
+
changed: true
|
|
1118
|
+
};
|
|
1119
|
+
} catch {
|
|
1120
|
+
return {
|
|
1121
|
+
content: existing,
|
|
1122
|
+
changed: false
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function ensureWorkflowConcurrency(existing, concurrency) {
|
|
1127
|
+
if (isToolingIgnored(existing)) return {
|
|
1128
|
+
content: existing,
|
|
1129
|
+
changed: false
|
|
1130
|
+
};
|
|
1131
|
+
try {
|
|
1132
|
+
const doc = parseDocument(existing);
|
|
1133
|
+
if (doc.has("concurrency")) return {
|
|
1134
|
+
content: existing,
|
|
1135
|
+
changed: false
|
|
1136
|
+
};
|
|
1137
|
+
doc.set("concurrency", concurrency);
|
|
1138
|
+
const contents = doc.contents;
|
|
1139
|
+
if (isMap(contents)) {
|
|
1140
|
+
const items = contents.items;
|
|
1141
|
+
const nameIdx = items.findIndex((p) => isScalar(p.key) && p.key.value === "name");
|
|
1142
|
+
const concPair = items.pop();
|
|
1143
|
+
if (concPair) items.splice(nameIdx + 1, 0, concPair);
|
|
1144
|
+
}
|
|
1145
|
+
return {
|
|
1146
|
+
content: doc.toString(),
|
|
1147
|
+
changed: true
|
|
1148
|
+
};
|
|
1149
|
+
} catch {
|
|
1150
|
+
return {
|
|
1151
|
+
content: existing,
|
|
1152
|
+
changed: false
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
//#endregion
|
|
1157
|
+
//#region src/generators/ci-utils.ts
|
|
1158
|
+
/** Build a GitHub Actions expression like `${{ expr }}` without triggering no-template-curly-in-string. */
|
|
1159
|
+
function actionsExpr(expr) {
|
|
1160
|
+
return `\${{ ${expr} }}`;
|
|
1161
|
+
}
|
|
1162
|
+
function hasEnginesNode(ctx) {
|
|
1163
|
+
const raw = ctx.read("package.json");
|
|
1164
|
+
if (!raw) return false;
|
|
1165
|
+
return typeof parsePackageJson(raw)?.engines?.["node"] === "string";
|
|
1166
|
+
}
|
|
1167
|
+
function computeNodeVersionYaml(ctx) {
|
|
1168
|
+
return hasEnginesNode(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
|
|
1169
|
+
}
|
|
1170
|
+
//#endregion
|
|
1171
|
+
//#region src/generators/publish-ci.ts
|
|
1172
|
+
function deployWorkflow(ci, nodeVersionYaml) {
|
|
1173
|
+
return `${workflowSchemaComment(ci)}name: Publish
|
|
1174
|
+
on:
|
|
1175
|
+
push:
|
|
1176
|
+
tags:
|
|
1177
|
+
- "v[0-9]+.[0-9]+.[0-9]+"
|
|
1178
|
+
|
|
1179
|
+
jobs:
|
|
1180
|
+
publish:
|
|
823
1181
|
runs-on: ubuntu-latest
|
|
824
1182
|
steps:
|
|
825
1183
|
- uses: actions/checkout@v4
|
|
@@ -830,10 +1188,10 @@ jobs:
|
|
|
830
1188
|
- run: pnpm install --frozen-lockfile
|
|
831
1189
|
- name: Publish Docker images
|
|
832
1190
|
env:
|
|
833
|
-
DOCKER_REGISTRY_HOST: ${actionsExpr
|
|
834
|
-
DOCKER_REGISTRY_NAMESPACE: ${actionsExpr
|
|
835
|
-
DOCKER_USERNAME: ${actionsExpr
|
|
836
|
-
DOCKER_PASSWORD: ${actionsExpr
|
|
1191
|
+
DOCKER_REGISTRY_HOST: ${actionsExpr("vars.DOCKER_REGISTRY_HOST")}
|
|
1192
|
+
DOCKER_REGISTRY_NAMESPACE: ${actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")}
|
|
1193
|
+
DOCKER_USERNAME: ${actionsExpr("secrets.DOCKER_USERNAME")}
|
|
1194
|
+
DOCKER_PASSWORD: ${actionsExpr("secrets.DOCKER_PASSWORD")}
|
|
837
1195
|
run: pnpm exec bst docker:publish
|
|
838
1196
|
`;
|
|
839
1197
|
}
|
|
@@ -861,8 +1219,6 @@ function requiredDeploySteps() {
|
|
|
861
1219
|
}
|
|
862
1220
|
];
|
|
863
1221
|
}
|
|
864
|
-
/** Convention paths to check for Dockerfiles. */
|
|
865
|
-
const CONVENTION_DOCKERFILE_PATHS$1 = ["Dockerfile", "docker/Dockerfile"];
|
|
866
1222
|
const DockerMapSchema = z.object({ docker: z.record(z.string(), z.unknown()).optional() });
|
|
867
1223
|
/** Get names of packages that have Docker builds (by convention or .tooling.json config). */
|
|
868
1224
|
function getDockerPackageNames(ctx) {
|
|
@@ -877,12 +1233,12 @@ function getDockerPackageNames(ctx) {
|
|
|
877
1233
|
for (const pkg of packages) {
|
|
878
1234
|
const dirName = pkg.name.split("/").pop() ?? pkg.name;
|
|
879
1235
|
if (names.includes(dirName)) continue;
|
|
880
|
-
for (const rel of CONVENTION_DOCKERFILE_PATHS
|
|
1236
|
+
for (const rel of CONVENTION_DOCKERFILE_PATHS) if (ctx.exists(`packages/${dirName}/${rel}`)) {
|
|
881
1237
|
names.push(dirName);
|
|
882
1238
|
break;
|
|
883
1239
|
}
|
|
884
1240
|
}
|
|
885
|
-
} else for (const rel of CONVENTION_DOCKERFILE_PATHS
|
|
1241
|
+
} else for (const rel of CONVENTION_DOCKERFILE_PATHS) if (ctx.exists(rel)) {
|
|
886
1242
|
if (!names.includes(ctx.config.name)) names.push(ctx.config.name);
|
|
887
1243
|
break;
|
|
888
1244
|
}
|
|
@@ -901,7 +1257,7 @@ async function generateDeployCi(ctx) {
|
|
|
901
1257
|
};
|
|
902
1258
|
const isGitHub = ctx.config.ci === "github";
|
|
903
1259
|
const workflowPath = isGitHub ? ".github/workflows/publish.yml" : ".forgejo/workflows/publish.yml";
|
|
904
|
-
const nodeVersionYaml =
|
|
1260
|
+
const nodeVersionYaml = computeNodeVersionYaml(ctx);
|
|
905
1261
|
const content = deployWorkflow(ctx.config.ci, nodeVersionYaml);
|
|
906
1262
|
if (ctx.exists(workflowPath)) {
|
|
907
1263
|
const raw = ctx.read(workflowPath);
|
|
@@ -1005,16 +1361,121 @@ function matchesManagedScript(scriptValue, expectedFragment) {
|
|
|
1005
1361
|
const DEPRECATED_SCRIPTS = ["tooling:init", "tooling:update"];
|
|
1006
1362
|
/** DevDeps that belong in every project (single repo) or per-package (monorepo). */
|
|
1007
1363
|
const PER_PACKAGE_DEV_DEPS = {
|
|
1008
|
-
"@types/node":
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1364
|
+
"@types/node": {
|
|
1365
|
+
"@changesets/cli": "2.30.0",
|
|
1366
|
+
"@release-it/bumper": "7.0.5",
|
|
1367
|
+
"commit-and-tag-version": "12.7.0",
|
|
1368
|
+
"knip": "5.87.0",
|
|
1369
|
+
"lefthook": "2.1.4",
|
|
1370
|
+
"oxfmt": "0.41.0",
|
|
1371
|
+
"oxlint": "1.56.0",
|
|
1372
|
+
"prettier": "3.8.1",
|
|
1373
|
+
"release-it": "19.2.4",
|
|
1374
|
+
"@types/node": "24.12.0",
|
|
1375
|
+
"tsdown": "0.21.4",
|
|
1376
|
+
"typescript": "5.9.3",
|
|
1377
|
+
"vitest": "4.1.0",
|
|
1378
|
+
"pnpm": "10.32.1"
|
|
1379
|
+
}["@types/node"],
|
|
1380
|
+
tsdown: {
|
|
1381
|
+
"@changesets/cli": "2.30.0",
|
|
1382
|
+
"@release-it/bumper": "7.0.5",
|
|
1383
|
+
"commit-and-tag-version": "12.7.0",
|
|
1384
|
+
"knip": "5.87.0",
|
|
1385
|
+
"lefthook": "2.1.4",
|
|
1386
|
+
"oxfmt": "0.41.0",
|
|
1387
|
+
"oxlint": "1.56.0",
|
|
1388
|
+
"prettier": "3.8.1",
|
|
1389
|
+
"release-it": "19.2.4",
|
|
1390
|
+
"@types/node": "24.12.0",
|
|
1391
|
+
"tsdown": "0.21.4",
|
|
1392
|
+
"typescript": "5.9.3",
|
|
1393
|
+
"vitest": "4.1.0",
|
|
1394
|
+
"pnpm": "10.32.1"
|
|
1395
|
+
}["tsdown"],
|
|
1396
|
+
typescript: {
|
|
1397
|
+
"@changesets/cli": "2.30.0",
|
|
1398
|
+
"@release-it/bumper": "7.0.5",
|
|
1399
|
+
"commit-and-tag-version": "12.7.0",
|
|
1400
|
+
"knip": "5.87.0",
|
|
1401
|
+
"lefthook": "2.1.4",
|
|
1402
|
+
"oxfmt": "0.41.0",
|
|
1403
|
+
"oxlint": "1.56.0",
|
|
1404
|
+
"prettier": "3.8.1",
|
|
1405
|
+
"release-it": "19.2.4",
|
|
1406
|
+
"@types/node": "24.12.0",
|
|
1407
|
+
"tsdown": "0.21.4",
|
|
1408
|
+
"typescript": "5.9.3",
|
|
1409
|
+
"vitest": "4.1.0",
|
|
1410
|
+
"pnpm": "10.32.1"
|
|
1411
|
+
}["typescript"],
|
|
1412
|
+
vitest: {
|
|
1413
|
+
"@changesets/cli": "2.30.0",
|
|
1414
|
+
"@release-it/bumper": "7.0.5",
|
|
1415
|
+
"commit-and-tag-version": "12.7.0",
|
|
1416
|
+
"knip": "5.87.0",
|
|
1417
|
+
"lefthook": "2.1.4",
|
|
1418
|
+
"oxfmt": "0.41.0",
|
|
1419
|
+
"oxlint": "1.56.0",
|
|
1420
|
+
"prettier": "3.8.1",
|
|
1421
|
+
"release-it": "19.2.4",
|
|
1422
|
+
"@types/node": "24.12.0",
|
|
1423
|
+
"tsdown": "0.21.4",
|
|
1424
|
+
"typescript": "5.9.3",
|
|
1425
|
+
"vitest": "4.1.0",
|
|
1426
|
+
"pnpm": "10.32.1"
|
|
1427
|
+
}["vitest"]
|
|
1012
1428
|
};
|
|
1013
1429
|
/** DevDeps that belong at the root regardless of structure. */
|
|
1014
1430
|
const ROOT_DEV_DEPS = {
|
|
1015
|
-
knip:
|
|
1016
|
-
|
|
1017
|
-
|
|
1431
|
+
knip: {
|
|
1432
|
+
"@changesets/cli": "2.30.0",
|
|
1433
|
+
"@release-it/bumper": "7.0.5",
|
|
1434
|
+
"commit-and-tag-version": "12.7.0",
|
|
1435
|
+
"knip": "5.87.0",
|
|
1436
|
+
"lefthook": "2.1.4",
|
|
1437
|
+
"oxfmt": "0.41.0",
|
|
1438
|
+
"oxlint": "1.56.0",
|
|
1439
|
+
"prettier": "3.8.1",
|
|
1440
|
+
"release-it": "19.2.4",
|
|
1441
|
+
"@types/node": "24.12.0",
|
|
1442
|
+
"tsdown": "0.21.4",
|
|
1443
|
+
"typescript": "5.9.3",
|
|
1444
|
+
"vitest": "4.1.0",
|
|
1445
|
+
"pnpm": "10.32.1"
|
|
1446
|
+
}["knip"],
|
|
1447
|
+
lefthook: {
|
|
1448
|
+
"@changesets/cli": "2.30.0",
|
|
1449
|
+
"@release-it/bumper": "7.0.5",
|
|
1450
|
+
"commit-and-tag-version": "12.7.0",
|
|
1451
|
+
"knip": "5.87.0",
|
|
1452
|
+
"lefthook": "2.1.4",
|
|
1453
|
+
"oxfmt": "0.41.0",
|
|
1454
|
+
"oxlint": "1.56.0",
|
|
1455
|
+
"prettier": "3.8.1",
|
|
1456
|
+
"release-it": "19.2.4",
|
|
1457
|
+
"@types/node": "24.12.0",
|
|
1458
|
+
"tsdown": "0.21.4",
|
|
1459
|
+
"typescript": "5.9.3",
|
|
1460
|
+
"vitest": "4.1.0",
|
|
1461
|
+
"pnpm": "10.32.1"
|
|
1462
|
+
}["lefthook"],
|
|
1463
|
+
oxlint: {
|
|
1464
|
+
"@changesets/cli": "2.30.0",
|
|
1465
|
+
"@release-it/bumper": "7.0.5",
|
|
1466
|
+
"commit-and-tag-version": "12.7.0",
|
|
1467
|
+
"knip": "5.87.0",
|
|
1468
|
+
"lefthook": "2.1.4",
|
|
1469
|
+
"oxfmt": "0.41.0",
|
|
1470
|
+
"oxlint": "1.56.0",
|
|
1471
|
+
"prettier": "3.8.1",
|
|
1472
|
+
"release-it": "19.2.4",
|
|
1473
|
+
"@types/node": "24.12.0",
|
|
1474
|
+
"tsdown": "0.21.4",
|
|
1475
|
+
"typescript": "5.9.3",
|
|
1476
|
+
"vitest": "4.1.0",
|
|
1477
|
+
"pnpm": "10.32.1"
|
|
1478
|
+
}["oxlint"]
|
|
1018
1479
|
};
|
|
1019
1480
|
/**
|
|
1020
1481
|
* Check if a package name is available as a workspace dependency.
|
|
@@ -1042,12 +1503,74 @@ const UPDATE_EXCLUDE = new Set(["@types/node"]);
|
|
|
1042
1503
|
function addReleaseDeps(deps, config) {
|
|
1043
1504
|
switch (config.releaseStrategy) {
|
|
1044
1505
|
case "release-it":
|
|
1045
|
-
deps["release-it"] =
|
|
1046
|
-
|
|
1506
|
+
deps["release-it"] = {
|
|
1507
|
+
"@changesets/cli": "2.30.0",
|
|
1508
|
+
"@release-it/bumper": "7.0.5",
|
|
1509
|
+
"commit-and-tag-version": "12.7.0",
|
|
1510
|
+
"knip": "5.87.0",
|
|
1511
|
+
"lefthook": "2.1.4",
|
|
1512
|
+
"oxfmt": "0.41.0",
|
|
1513
|
+
"oxlint": "1.56.0",
|
|
1514
|
+
"prettier": "3.8.1",
|
|
1515
|
+
"release-it": "19.2.4",
|
|
1516
|
+
"@types/node": "24.12.0",
|
|
1517
|
+
"tsdown": "0.21.4",
|
|
1518
|
+
"typescript": "5.9.3",
|
|
1519
|
+
"vitest": "4.1.0",
|
|
1520
|
+
"pnpm": "10.32.1"
|
|
1521
|
+
}["release-it"];
|
|
1522
|
+
if (config.structure === "monorepo") deps["@release-it/bumper"] = {
|
|
1523
|
+
"@changesets/cli": "2.30.0",
|
|
1524
|
+
"@release-it/bumper": "7.0.5",
|
|
1525
|
+
"commit-and-tag-version": "12.7.0",
|
|
1526
|
+
"knip": "5.87.0",
|
|
1527
|
+
"lefthook": "2.1.4",
|
|
1528
|
+
"oxfmt": "0.41.0",
|
|
1529
|
+
"oxlint": "1.56.0",
|
|
1530
|
+
"prettier": "3.8.1",
|
|
1531
|
+
"release-it": "19.2.4",
|
|
1532
|
+
"@types/node": "24.12.0",
|
|
1533
|
+
"tsdown": "0.21.4",
|
|
1534
|
+
"typescript": "5.9.3",
|
|
1535
|
+
"vitest": "4.1.0",
|
|
1536
|
+
"pnpm": "10.32.1"
|
|
1537
|
+
}["@release-it/bumper"];
|
|
1538
|
+
break;
|
|
1539
|
+
case "simple":
|
|
1540
|
+
deps["commit-and-tag-version"] = {
|
|
1541
|
+
"@changesets/cli": "2.30.0",
|
|
1542
|
+
"@release-it/bumper": "7.0.5",
|
|
1543
|
+
"commit-and-tag-version": "12.7.0",
|
|
1544
|
+
"knip": "5.87.0",
|
|
1545
|
+
"lefthook": "2.1.4",
|
|
1546
|
+
"oxfmt": "0.41.0",
|
|
1547
|
+
"oxlint": "1.56.0",
|
|
1548
|
+
"prettier": "3.8.1",
|
|
1549
|
+
"release-it": "19.2.4",
|
|
1550
|
+
"@types/node": "24.12.0",
|
|
1551
|
+
"tsdown": "0.21.4",
|
|
1552
|
+
"typescript": "5.9.3",
|
|
1553
|
+
"vitest": "4.1.0",
|
|
1554
|
+
"pnpm": "10.32.1"
|
|
1555
|
+
}["commit-and-tag-version"];
|
|
1047
1556
|
break;
|
|
1048
|
-
case "simple": break;
|
|
1049
1557
|
case "changesets":
|
|
1050
|
-
deps["@changesets/cli"] =
|
|
1558
|
+
deps["@changesets/cli"] = {
|
|
1559
|
+
"@changesets/cli": "2.30.0",
|
|
1560
|
+
"@release-it/bumper": "7.0.5",
|
|
1561
|
+
"commit-and-tag-version": "12.7.0",
|
|
1562
|
+
"knip": "5.87.0",
|
|
1563
|
+
"lefthook": "2.1.4",
|
|
1564
|
+
"oxfmt": "0.41.0",
|
|
1565
|
+
"oxlint": "1.56.0",
|
|
1566
|
+
"prettier": "3.8.1",
|
|
1567
|
+
"release-it": "19.2.4",
|
|
1568
|
+
"@types/node": "24.12.0",
|
|
1569
|
+
"tsdown": "0.21.4",
|
|
1570
|
+
"typescript": "5.9.3",
|
|
1571
|
+
"vitest": "4.1.0",
|
|
1572
|
+
"pnpm": "10.32.1"
|
|
1573
|
+
}["@changesets/cli"];
|
|
1051
1574
|
break;
|
|
1052
1575
|
}
|
|
1053
1576
|
}
|
|
@@ -1056,9 +1579,39 @@ function getAddedDevDepNames(config) {
|
|
|
1056
1579
|
const deps = { ...ROOT_DEV_DEPS };
|
|
1057
1580
|
if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
|
|
1058
1581
|
deps["@bensandee/config"] = "0.9.1";
|
|
1059
|
-
deps["@bensandee/tooling"] = "0.
|
|
1060
|
-
if (config.formatter === "oxfmt") deps["oxfmt"] =
|
|
1061
|
-
|
|
1582
|
+
deps["@bensandee/tooling"] = "0.29.0";
|
|
1583
|
+
if (config.formatter === "oxfmt") deps["oxfmt"] = {
|
|
1584
|
+
"@changesets/cli": "2.30.0",
|
|
1585
|
+
"@release-it/bumper": "7.0.5",
|
|
1586
|
+
"commit-and-tag-version": "12.7.0",
|
|
1587
|
+
"knip": "5.87.0",
|
|
1588
|
+
"lefthook": "2.1.4",
|
|
1589
|
+
"oxfmt": "0.41.0",
|
|
1590
|
+
"oxlint": "1.56.0",
|
|
1591
|
+
"prettier": "3.8.1",
|
|
1592
|
+
"release-it": "19.2.4",
|
|
1593
|
+
"@types/node": "24.12.0",
|
|
1594
|
+
"tsdown": "0.21.4",
|
|
1595
|
+
"typescript": "5.9.3",
|
|
1596
|
+
"vitest": "4.1.0",
|
|
1597
|
+
"pnpm": "10.32.1"
|
|
1598
|
+
}["oxfmt"];
|
|
1599
|
+
if (config.formatter === "prettier") deps["prettier"] = {
|
|
1600
|
+
"@changesets/cli": "2.30.0",
|
|
1601
|
+
"@release-it/bumper": "7.0.5",
|
|
1602
|
+
"commit-and-tag-version": "12.7.0",
|
|
1603
|
+
"knip": "5.87.0",
|
|
1604
|
+
"lefthook": "2.1.4",
|
|
1605
|
+
"oxfmt": "0.41.0",
|
|
1606
|
+
"oxlint": "1.56.0",
|
|
1607
|
+
"prettier": "3.8.1",
|
|
1608
|
+
"release-it": "19.2.4",
|
|
1609
|
+
"@types/node": "24.12.0",
|
|
1610
|
+
"tsdown": "0.21.4",
|
|
1611
|
+
"typescript": "5.9.3",
|
|
1612
|
+
"vitest": "4.1.0",
|
|
1613
|
+
"pnpm": "10.32.1"
|
|
1614
|
+
}["prettier"];
|
|
1062
1615
|
addReleaseDeps(deps, config);
|
|
1063
1616
|
return Object.keys(deps).filter((name) => !UPDATE_EXCLUDE.has(name));
|
|
1064
1617
|
}
|
|
@@ -1081,10 +1634,40 @@ async function generatePackageJson(ctx) {
|
|
|
1081
1634
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1082
1635
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1083
1636
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.1";
|
|
1084
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1637
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.29.0";
|
|
1085
1638
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
|
|
1086
|
-
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] =
|
|
1087
|
-
|
|
1639
|
+
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1640
|
+
"@changesets/cli": "2.30.0",
|
|
1641
|
+
"@release-it/bumper": "7.0.5",
|
|
1642
|
+
"commit-and-tag-version": "12.7.0",
|
|
1643
|
+
"knip": "5.87.0",
|
|
1644
|
+
"lefthook": "2.1.4",
|
|
1645
|
+
"oxfmt": "0.41.0",
|
|
1646
|
+
"oxlint": "1.56.0",
|
|
1647
|
+
"prettier": "3.8.1",
|
|
1648
|
+
"release-it": "19.2.4",
|
|
1649
|
+
"@types/node": "24.12.0",
|
|
1650
|
+
"tsdown": "0.21.4",
|
|
1651
|
+
"typescript": "5.9.3",
|
|
1652
|
+
"vitest": "4.1.0",
|
|
1653
|
+
"pnpm": "10.32.1"
|
|
1654
|
+
}["oxfmt"];
|
|
1655
|
+
if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
|
|
1656
|
+
"@changesets/cli": "2.30.0",
|
|
1657
|
+
"@release-it/bumper": "7.0.5",
|
|
1658
|
+
"commit-and-tag-version": "12.7.0",
|
|
1659
|
+
"knip": "5.87.0",
|
|
1660
|
+
"lefthook": "2.1.4",
|
|
1661
|
+
"oxfmt": "0.41.0",
|
|
1662
|
+
"oxlint": "1.56.0",
|
|
1663
|
+
"prettier": "3.8.1",
|
|
1664
|
+
"release-it": "19.2.4",
|
|
1665
|
+
"@types/node": "24.12.0",
|
|
1666
|
+
"tsdown": "0.21.4",
|
|
1667
|
+
"typescript": "5.9.3",
|
|
1668
|
+
"vitest": "4.1.0",
|
|
1669
|
+
"pnpm": "10.32.1"
|
|
1670
|
+
}["prettier"];
|
|
1088
1671
|
addReleaseDeps(devDeps, ctx.config);
|
|
1089
1672
|
if (existing) {
|
|
1090
1673
|
const pkg = parsePackageJson(existing);
|
|
@@ -1124,6 +1707,13 @@ async function generatePackageJson(ctx) {
|
|
|
1124
1707
|
changes.push(`updated devDependency: ${key} to ${value}`);
|
|
1125
1708
|
}
|
|
1126
1709
|
pkg.devDependencies = existingDevDeps;
|
|
1710
|
+
if (!pkg.repository) {
|
|
1711
|
+
const repoUrl = detectGitRemoteUrl(ctx.targetDir);
|
|
1712
|
+
if (repoUrl) {
|
|
1713
|
+
pkg.repository = repoUrl;
|
|
1714
|
+
changes.push("added repository from git remote");
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1127
1717
|
if (!pkg["engines"]) {
|
|
1128
1718
|
pkg["engines"] = { node: ">=24.13.0" };
|
|
1129
1719
|
changes.push("set engines.node >= 24.13.0");
|
|
@@ -1140,15 +1730,32 @@ async function generatePackageJson(ctx) {
|
|
|
1140
1730
|
description: changes.join(", ")
|
|
1141
1731
|
};
|
|
1142
1732
|
}
|
|
1733
|
+
const repoUrl = detectGitRemoteUrl(ctx.targetDir);
|
|
1143
1734
|
const pkg = {
|
|
1144
1735
|
name: ctx.config.name,
|
|
1145
1736
|
version: "0.1.0",
|
|
1146
1737
|
private: true,
|
|
1738
|
+
...repoUrl && { repository: repoUrl },
|
|
1147
1739
|
type: "module",
|
|
1148
1740
|
scripts: allScripts,
|
|
1149
1741
|
devDependencies: devDeps,
|
|
1150
1742
|
engines: { node: ">=24.13.0" },
|
|
1151
|
-
packageManager:
|
|
1743
|
+
packageManager: `pnpm@${{
|
|
1744
|
+
"@changesets/cli": "2.30.0",
|
|
1745
|
+
"@release-it/bumper": "7.0.5",
|
|
1746
|
+
"commit-and-tag-version": "12.7.0",
|
|
1747
|
+
"knip": "5.87.0",
|
|
1748
|
+
"lefthook": "2.1.4",
|
|
1749
|
+
"oxfmt": "0.41.0",
|
|
1750
|
+
"oxlint": "1.56.0",
|
|
1751
|
+
"prettier": "3.8.1",
|
|
1752
|
+
"release-it": "19.2.4",
|
|
1753
|
+
"@types/node": "24.12.0",
|
|
1754
|
+
"tsdown": "0.21.4",
|
|
1755
|
+
"typescript": "5.9.3",
|
|
1756
|
+
"vitest": "4.1.0",
|
|
1757
|
+
"pnpm": "10.32.1"
|
|
1758
|
+
}["pnpm"]}`
|
|
1152
1759
|
};
|
|
1153
1760
|
ctx.write(filePath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1154
1761
|
return {
|
|
@@ -1565,30 +2172,23 @@ async function generateGitignore(ctx) {
|
|
|
1565
2172
|
}
|
|
1566
2173
|
//#endregion
|
|
1567
2174
|
//#region src/generators/ci.ts
|
|
1568
|
-
/** Build a GitHub Actions expression like `${{ expr }}` without triggering no-template-curly-in-string. */
|
|
1569
|
-
function actionsExpr$1(expr) {
|
|
1570
|
-
return `\${{ ${expr} }}`;
|
|
1571
|
-
}
|
|
1572
2175
|
const CI_CONCURRENCY = {
|
|
1573
|
-
group: `ci-${actionsExpr
|
|
1574
|
-
"cancel-in-progress": actionsExpr
|
|
2176
|
+
group: `ci-${actionsExpr("github.ref")}`,
|
|
2177
|
+
"cancel-in-progress": actionsExpr("github.ref != 'refs/heads/main'")
|
|
1575
2178
|
};
|
|
1576
|
-
function hasEnginesNode$1(ctx) {
|
|
1577
|
-
const raw = ctx.read("package.json");
|
|
1578
|
-
if (!raw) return false;
|
|
1579
|
-
return typeof parsePackageJson(raw)?.engines?.["node"] === "string";
|
|
1580
|
-
}
|
|
1581
2179
|
function ciWorkflow(nodeVersionYaml, isForgejo, isChangesets) {
|
|
1582
2180
|
const emailNotifications = isForgejo ? "\nenable-email-notifications: true\n" : "";
|
|
1583
2181
|
const concurrencyBlock = isChangesets ? `
|
|
1584
2182
|
concurrency:
|
|
1585
|
-
group: ci-${actionsExpr
|
|
1586
|
-
cancel-in-progress: ${actionsExpr
|
|
2183
|
+
group: ci-${actionsExpr("github.ref")}
|
|
2184
|
+
cancel-in-progress: ${actionsExpr("github.ref != 'refs/heads/main'")}
|
|
1587
2185
|
` : "";
|
|
1588
2186
|
return `${workflowSchemaComment(isForgejo ? "forgejo" : "github")}name: CI
|
|
1589
2187
|
${emailNotifications}on:
|
|
1590
2188
|
push:
|
|
1591
2189
|
branches: [main]
|
|
2190
|
+
tags-ignore:
|
|
2191
|
+
- "**"
|
|
1592
2192
|
pull_request:
|
|
1593
2193
|
${concurrencyBlock}
|
|
1594
2194
|
jobs:
|
|
@@ -1651,13 +2251,18 @@ async function generateCi(ctx) {
|
|
|
1651
2251
|
};
|
|
1652
2252
|
const isGitHub = ctx.config.ci === "github";
|
|
1653
2253
|
const isChangesets = ctx.config.releaseStrategy === "changesets";
|
|
1654
|
-
const nodeVersionYaml =
|
|
2254
|
+
const nodeVersionYaml = computeNodeVersionYaml(ctx);
|
|
1655
2255
|
const filePath = ciWorkflowPath(ctx.config.ci, ctx.config.releaseStrategy);
|
|
1656
2256
|
const content = ciWorkflow(nodeVersionYaml, !isGitHub, isChangesets);
|
|
1657
2257
|
if (ctx.exists(filePath)) {
|
|
1658
2258
|
const existing = ctx.read(filePath);
|
|
1659
2259
|
if (existing) {
|
|
1660
2260
|
let result = mergeWorkflowSteps(existing, "check", requiredCheckSteps(nodeVersionYaml));
|
|
2261
|
+
const withTagsIgnore = ensureWorkflowTagsIgnore(result.content);
|
|
2262
|
+
result = {
|
|
2263
|
+
content: withTagsIgnore.content,
|
|
2264
|
+
changed: result.changed || withTagsIgnore.changed
|
|
2265
|
+
};
|
|
1661
2266
|
if (isChangesets) {
|
|
1662
2267
|
const withConcurrency = ensureWorkflowConcurrency(result.content, CI_CONCURRENCY);
|
|
1663
2268
|
result = {
|
|
@@ -2155,16 +2760,9 @@ async function generateChangesets(ctx) {
|
|
|
2155
2760
|
description: "Generated .changeset/config.json"
|
|
2156
2761
|
};
|
|
2157
2762
|
}
|
|
2158
|
-
//#endregion
|
|
2159
|
-
//#region src/generators/release-ci.ts
|
|
2160
|
-
|
|
2161
|
-
function actionsExpr(expr) {
|
|
2162
|
-
return `\${{ ${expr} }}`;
|
|
2163
|
-
}
|
|
2164
|
-
function hasEnginesNode(ctx) {
|
|
2165
|
-
return typeof ctx.packageJson?.["engines"]?.["node"] === "string";
|
|
2166
|
-
}
|
|
2167
|
-
function commonSteps(nodeVersionYaml, publishesNpm, { build = true } = {}) {
|
|
2763
|
+
//#endregion
|
|
2764
|
+
//#region src/generators/release-ci.ts
|
|
2765
|
+
function commonSteps(nodeVersionYaml, publishesNpm) {
|
|
2168
2766
|
return ` - uses: actions/checkout@v4
|
|
2169
2767
|
with:
|
|
2170
2768
|
fetch-depth: 0
|
|
@@ -2173,7 +2771,7 @@ function commonSteps(nodeVersionYaml, publishesNpm, { build = true } = {}) {
|
|
|
2173
2771
|
with:
|
|
2174
2772
|
${nodeVersionYaml}
|
|
2175
2773
|
cache: pnpm${publishesNpm ? `\n registry-url: "https://registry.npmjs.org"` : ""}
|
|
2176
|
-
- run: pnpm install --frozen-lockfile
|
|
2774
|
+
- run: pnpm install --frozen-lockfile`;
|
|
2177
2775
|
}
|
|
2178
2776
|
function releaseItWorkflow(ci, nodeVersionYaml, publishesNpm) {
|
|
2179
2777
|
const isGitHub = ci === "github";
|
|
@@ -2227,7 +2825,7 @@ jobs:
|
|
|
2227
2825
|
release:
|
|
2228
2826
|
runs-on: ubuntu-latest
|
|
2229
2827
|
steps:
|
|
2230
|
-
${commonSteps(nodeVersionYaml, publishesNpm
|
|
2828
|
+
${commonSteps(nodeVersionYaml, publishesNpm)}${gitConfigStep}${releaseStep}
|
|
2231
2829
|
`;
|
|
2232
2830
|
}
|
|
2233
2831
|
/** Build the required release step for the check job (changesets). */
|
|
@@ -2290,11 +2888,7 @@ function requiredReleaseSteps(strategy, nodeVersionYaml, publishesNpm) {
|
|
|
2290
2888
|
{
|
|
2291
2889
|
match: { run: "pnpm install" },
|
|
2292
2890
|
step: { run: "pnpm install --frozen-lockfile" }
|
|
2293
|
-
}
|
|
2294
|
-
...strategy !== "simple" ? [{
|
|
2295
|
-
match: { run: "build" },
|
|
2296
|
-
step: { run: "pnpm build" }
|
|
2297
|
-
}] : []
|
|
2891
|
+
}
|
|
2298
2892
|
];
|
|
2299
2893
|
switch (strategy) {
|
|
2300
2894
|
case "release-it":
|
|
@@ -2370,7 +2964,7 @@ async function generateReleaseCi(ctx) {
|
|
|
2370
2964
|
if (ctx.config.releaseStrategy === "changesets") return generateChangesetsReleaseCi(ctx, publishesNpm);
|
|
2371
2965
|
const isGitHub = ctx.config.ci === "github";
|
|
2372
2966
|
const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
|
|
2373
|
-
const nodeVersionYaml =
|
|
2967
|
+
const nodeVersionYaml = computeNodeVersionYaml(ctx);
|
|
2374
2968
|
const content = buildWorkflow(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm);
|
|
2375
2969
|
if (!content) return {
|
|
2376
2970
|
filePath,
|
|
@@ -2594,407 +3188,125 @@ const SCHEMA_GLOB = ".forgejo/workflows/*.{yml,yaml}";
|
|
|
2594
3188
|
const VscodeSettingsSchema = z.looseObject({ "yaml.schemas": z.record(z.string(), z.unknown()).default({}) });
|
|
2595
3189
|
function readSchemaFromNodeModules(targetDir) {
|
|
2596
3190
|
const candidate = path.join(targetDir, "node_modules", SCHEMA_NPM_PATH);
|
|
2597
|
-
if (!existsSync(candidate)) return void 0;
|
|
2598
|
-
return readFileSync(candidate, "utf-8");
|
|
2599
|
-
}
|
|
2600
|
-
function serializeSettings(settings) {
|
|
2601
|
-
return JSON.stringify(settings, null, 2) + "\n";
|
|
2602
|
-
}
|
|
2603
|
-
const YamlSchemasSchema = z.record(z.string(), z.unknown());
|
|
2604
|
-
/** Merge yaml.schemas into a settings object. Returns the result and whether anything changed. */
|
|
2605
|
-
function mergeYamlSchemas(settings) {
|
|
2606
|
-
const parsed = YamlSchemasSchema.safeParse(settings["yaml.schemas"]);
|
|
2607
|
-
const yamlSchemas = parsed.success ? { ...parsed.data } : {};
|
|
2608
|
-
if (SCHEMA_LOCAL_PATH in yamlSchemas) return {
|
|
2609
|
-
merged: settings,
|
|
2610
|
-
changed: false
|
|
2611
|
-
};
|
|
2612
|
-
yamlSchemas[SCHEMA_LOCAL_PATH] = SCHEMA_GLOB;
|
|
2613
|
-
return {
|
|
2614
|
-
merged: {
|
|
2615
|
-
...settings,
|
|
2616
|
-
"yaml.schemas": yamlSchemas
|
|
2617
|
-
},
|
|
2618
|
-
changed: true
|
|
2619
|
-
};
|
|
2620
|
-
}
|
|
2621
|
-
function writeSchemaToSettings(ctx) {
|
|
2622
|
-
if (ctx.exists(SETTINGS_PATH)) {
|
|
2623
|
-
const raw = ctx.read(SETTINGS_PATH);
|
|
2624
|
-
if (!raw) return {
|
|
2625
|
-
filePath: SETTINGS_PATH,
|
|
2626
|
-
action: "skipped",
|
|
2627
|
-
description: "Could not read existing settings"
|
|
2628
|
-
};
|
|
2629
|
-
const parsed = VscodeSettingsSchema.safeParse(parse(raw));
|
|
2630
|
-
if (!parsed.success) return {
|
|
2631
|
-
filePath: SETTINGS_PATH,
|
|
2632
|
-
action: "skipped",
|
|
2633
|
-
description: "Could not parse existing settings"
|
|
2634
|
-
};
|
|
2635
|
-
const { merged, changed } = mergeYamlSchemas(parsed.data);
|
|
2636
|
-
if (!changed) return {
|
|
2637
|
-
filePath: SETTINGS_PATH,
|
|
2638
|
-
action: "skipped",
|
|
2639
|
-
description: "Already has Forgejo schema mapping"
|
|
2640
|
-
};
|
|
2641
|
-
ctx.write(SETTINGS_PATH, serializeSettings(merged));
|
|
2642
|
-
return {
|
|
2643
|
-
filePath: SETTINGS_PATH,
|
|
2644
|
-
action: "updated",
|
|
2645
|
-
description: "Added Forgejo workflow schema mapping"
|
|
2646
|
-
};
|
|
2647
|
-
}
|
|
2648
|
-
ctx.write(SETTINGS_PATH, serializeSettings({ "yaml.schemas": { [SCHEMA_LOCAL_PATH]: SCHEMA_GLOB } }));
|
|
2649
|
-
return {
|
|
2650
|
-
filePath: SETTINGS_PATH,
|
|
2651
|
-
action: "created",
|
|
2652
|
-
description: "Generated .vscode/settings.json with Forgejo workflow schema"
|
|
2653
|
-
};
|
|
2654
|
-
}
|
|
2655
|
-
async function generateVscodeSettings(ctx) {
|
|
2656
|
-
const results = [];
|
|
2657
|
-
if (ctx.config.ci !== "forgejo") {
|
|
2658
|
-
results.push({
|
|
2659
|
-
filePath: SETTINGS_PATH,
|
|
2660
|
-
action: "skipped",
|
|
2661
|
-
description: "Not a Forgejo project"
|
|
2662
|
-
});
|
|
2663
|
-
return results;
|
|
2664
|
-
}
|
|
2665
|
-
const schemaContent = readSchemaFromNodeModules(ctx.targetDir);
|
|
2666
|
-
if (!schemaContent) {
|
|
2667
|
-
results.push({
|
|
2668
|
-
filePath: SCHEMA_LOCAL_PATH,
|
|
2669
|
-
action: "skipped",
|
|
2670
|
-
description: "Could not find @bensandee/config schema in node_modules"
|
|
2671
|
-
});
|
|
2672
|
-
return results;
|
|
2673
|
-
}
|
|
2674
|
-
const existingSchema = ctx.read(SCHEMA_LOCAL_PATH);
|
|
2675
|
-
if (existingSchema !== void 0 && contentEqual(SCHEMA_LOCAL_PATH, existingSchema, schemaContent)) results.push({
|
|
2676
|
-
filePath: SCHEMA_LOCAL_PATH,
|
|
2677
|
-
action: "skipped",
|
|
2678
|
-
description: "Schema already up to date"
|
|
2679
|
-
});
|
|
2680
|
-
else {
|
|
2681
|
-
ctx.write(SCHEMA_LOCAL_PATH, schemaContent);
|
|
2682
|
-
results.push({
|
|
2683
|
-
filePath: SCHEMA_LOCAL_PATH,
|
|
2684
|
-
action: existingSchema ? "updated" : "created",
|
|
2685
|
-
description: "Copied Forgejo workflow schema from @bensandee/config"
|
|
2686
|
-
});
|
|
2687
|
-
}
|
|
2688
|
-
results.push(writeSchemaToSettings(ctx));
|
|
2689
|
-
return results;
|
|
2690
|
-
}
|
|
2691
|
-
//#endregion
|
|
2692
|
-
//#region src/generators/pipeline.ts
|
|
2693
|
-
/** Run all generators sequentially and return their results. */
|
|
2694
|
-
async function runGenerators(ctx) {
|
|
2695
|
-
const results = [];
|
|
2696
|
-
results.push(await generatePackageJson(ctx));
|
|
2697
|
-
results.push(await generatePnpmWorkspace(ctx));
|
|
2698
|
-
results.push(...await generateTsconfig(ctx));
|
|
2699
|
-
results.push(await generateTsdown(ctx));
|
|
2700
|
-
results.push(await generateOxlint(ctx));
|
|
2701
|
-
results.push(await generateFormatter(ctx));
|
|
2702
|
-
results.push(...await generateLefthook(ctx));
|
|
2703
|
-
results.push(await generateGitignore(ctx));
|
|
2704
|
-
results.push(await generateKnip(ctx));
|
|
2705
|
-
results.push(await generateRenovate(ctx));
|
|
2706
|
-
results.push(await generateCi(ctx));
|
|
2707
|
-
results.push(...await generateClaudeSettings(ctx));
|
|
2708
|
-
results.push(await generateReleaseIt(ctx));
|
|
2709
|
-
results.push(await generateChangesets(ctx));
|
|
2710
|
-
results.push(await generateReleaseCi(ctx));
|
|
2711
|
-
results.push(await generateDeployCi(ctx));
|
|
2712
|
-
results.push(...await generateVitest(ctx));
|
|
2713
|
-
results.push(...await generateVscodeSettings(ctx));
|
|
2714
|
-
results.push(saveToolingConfig(ctx, ctx.config));
|
|
2715
|
-
return results;
|
|
2716
|
-
}
|
|
2717
|
-
//#endregion
|
|
2718
|
-
//#region src/release/docker.ts
|
|
2719
|
-
const ToolingDockerMapSchema = z.record(z.string(), z.object({
|
|
2720
|
-
dockerfile: z.string(),
|
|
2721
|
-
context: z.string().default(".")
|
|
2722
|
-
}));
|
|
2723
|
-
const ToolingConfigDockerSchema = z.object({ docker: ToolingDockerMapSchema.optional() });
|
|
2724
|
-
const PackageInfoSchema = z.object({
|
|
2725
|
-
name: z.string().optional(),
|
|
2726
|
-
version: z.string().optional()
|
|
2727
|
-
});
|
|
2728
|
-
/** Read the docker map from .tooling.json. Returns empty record if missing or invalid. */
|
|
2729
|
-
function loadDockerMap(executor, cwd) {
|
|
2730
|
-
const configPath = path.join(cwd, ".tooling.json");
|
|
2731
|
-
const raw = executor.readFile(configPath);
|
|
2732
|
-
if (!raw) return {};
|
|
2733
|
-
try {
|
|
2734
|
-
const result = ToolingConfigDockerSchema.safeParse(JSON.parse(raw));
|
|
2735
|
-
if (!result.success || !result.data.docker) return {};
|
|
2736
|
-
return result.data.docker;
|
|
2737
|
-
} catch (_error) {
|
|
2738
|
-
return {};
|
|
2739
|
-
}
|
|
2740
|
-
}
|
|
2741
|
-
/** Read name and version from a package's package.json. */
|
|
2742
|
-
function readPackageInfo(executor, packageJsonPath) {
|
|
2743
|
-
const raw = executor.readFile(packageJsonPath);
|
|
2744
|
-
if (!raw) return {
|
|
2745
|
-
name: void 0,
|
|
2746
|
-
version: void 0
|
|
2747
|
-
};
|
|
2748
|
-
try {
|
|
2749
|
-
const result = PackageInfoSchema.safeParse(JSON.parse(raw));
|
|
2750
|
-
if (!result.success) return {
|
|
2751
|
-
name: void 0,
|
|
2752
|
-
version: void 0
|
|
2753
|
-
};
|
|
2754
|
-
return {
|
|
2755
|
-
name: result.data.name,
|
|
2756
|
-
version: result.data.version
|
|
2757
|
-
};
|
|
2758
|
-
} catch (_error) {
|
|
2759
|
-
return {
|
|
2760
|
-
name: void 0,
|
|
2761
|
-
version: void 0
|
|
2762
|
-
};
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
/** Strip npm scope from a package name: "@scope/foo" → "foo", "foo" → "foo". */
|
|
2766
|
-
function stripScope(name) {
|
|
2767
|
-
const slashIndex = name.indexOf("/");
|
|
2768
|
-
return name.startsWith("@") && slashIndex !== -1 ? name.slice(slashIndex + 1) : name;
|
|
2769
|
-
}
|
|
2770
|
-
/** Convention paths to check for Dockerfiles in a package directory. */
|
|
2771
|
-
const CONVENTION_DOCKERFILE_PATHS = ["Dockerfile", "docker/Dockerfile"];
|
|
2772
|
-
/**
|
|
2773
|
-
* Find a Dockerfile at convention paths for a monorepo package.
|
|
2774
|
-
* Checks packages/{dir}/Dockerfile and packages/{dir}/docker/Dockerfile.
|
|
2775
|
-
*/
|
|
2776
|
-
function findConventionDockerfile(executor, cwd, dir) {
|
|
2777
|
-
for (const rel of CONVENTION_DOCKERFILE_PATHS) {
|
|
2778
|
-
const dockerfilePath = `packages/${dir}/${rel}`;
|
|
2779
|
-
if (executor.readFile(path.join(cwd, dockerfilePath)) !== null) return {
|
|
2780
|
-
dockerfile: dockerfilePath,
|
|
2781
|
-
context: "."
|
|
2782
|
-
};
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
/**
|
|
2786
|
-
* Find a Dockerfile at convention paths for a single-package repo.
|
|
2787
|
-
* Checks Dockerfile and docker/Dockerfile at the project root.
|
|
2788
|
-
*/
|
|
2789
|
-
function findRootDockerfile(executor, cwd) {
|
|
2790
|
-
for (const rel of CONVENTION_DOCKERFILE_PATHS) if (executor.readFile(path.join(cwd, rel)) !== null) return {
|
|
2791
|
-
dockerfile: rel,
|
|
2792
|
-
context: "."
|
|
2793
|
-
};
|
|
2794
|
-
}
|
|
2795
|
-
/**
|
|
2796
|
-
* Discover Docker packages by convention and merge with .tooling.json overrides.
|
|
2797
|
-
*
|
|
2798
|
-
* Convention: any package with a Dockerfile or docker/Dockerfile is a Docker package.
|
|
2799
|
-
* For monorepos, scans packages/{name}/. For single-package repos, scans the root.
|
|
2800
|
-
* The docker map in .tooling.json overrides convention-discovered config and can add
|
|
2801
|
-
* packages at non-standard locations.
|
|
2802
|
-
*
|
|
2803
|
-
* Image names are derived from {root-name}-{package-name} using each package's package.json name.
|
|
2804
|
-
* Versions are read from each package's own package.json.
|
|
2805
|
-
*/
|
|
2806
|
-
function detectDockerPackages(executor, cwd, repoName) {
|
|
2807
|
-
const overrides = loadDockerMap(executor, cwd);
|
|
2808
|
-
const packageDirs = executor.listPackageDirs(cwd);
|
|
2809
|
-
const packages = [];
|
|
2810
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2811
|
-
if (packageDirs.length > 0) {
|
|
2812
|
-
for (const dir of packageDirs) {
|
|
2813
|
-
const convention = findConventionDockerfile(executor, cwd, dir);
|
|
2814
|
-
const docker = overrides[dir] ?? convention;
|
|
2815
|
-
if (docker) {
|
|
2816
|
-
const { name, version } = readPackageInfo(executor, path.join(cwd, "packages", dir, "package.json"));
|
|
2817
|
-
packages.push({
|
|
2818
|
-
dir,
|
|
2819
|
-
imageName: `${repoName}-${stripScope(name ?? dir)}`,
|
|
2820
|
-
version,
|
|
2821
|
-
docker
|
|
2822
|
-
});
|
|
2823
|
-
seen.add(dir);
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
for (const [dir, docker] of Object.entries(overrides)) if (!seen.has(dir)) {
|
|
2827
|
-
const { name, version } = readPackageInfo(executor, path.join(cwd, "packages", dir, "package.json"));
|
|
2828
|
-
packages.push({
|
|
2829
|
-
dir,
|
|
2830
|
-
imageName: `${repoName}-${stripScope(name ?? dir)}`,
|
|
2831
|
-
version,
|
|
2832
|
-
docker
|
|
2833
|
-
});
|
|
2834
|
-
}
|
|
2835
|
-
} else {
|
|
2836
|
-
const convention = findRootDockerfile(executor, cwd);
|
|
2837
|
-
const docker = overrides["."] ?? convention;
|
|
2838
|
-
if (docker) {
|
|
2839
|
-
const { name, version } = readPackageInfo(executor, path.join(cwd, "package.json"));
|
|
2840
|
-
packages.push({
|
|
2841
|
-
dir: ".",
|
|
2842
|
-
imageName: stripScope(name ?? repoName),
|
|
2843
|
-
version,
|
|
2844
|
-
docker
|
|
2845
|
-
});
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
return packages;
|
|
3191
|
+
if (!existsSync(candidate)) return void 0;
|
|
3192
|
+
return readFileSync(candidate, "utf-8");
|
|
2849
3193
|
}
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
const
|
|
2857
|
-
const
|
|
2858
|
-
if (
|
|
2859
|
-
|
|
3194
|
+
function serializeSettings(settings) {
|
|
3195
|
+
return JSON.stringify(settings, null, 2) + "\n";
|
|
3196
|
+
}
|
|
3197
|
+
const YamlSchemasSchema = z.record(z.string(), z.unknown());
|
|
3198
|
+
/** Merge yaml.schemas into a settings object. Returns the result and whether anything changed. */
|
|
3199
|
+
function mergeYamlSchemas(settings) {
|
|
3200
|
+
const parsed = YamlSchemasSchema.safeParse(settings["yaml.schemas"]);
|
|
3201
|
+
const yamlSchemas = parsed.success ? { ...parsed.data } : {};
|
|
3202
|
+
if (SCHEMA_LOCAL_PATH in yamlSchemas) return {
|
|
3203
|
+
merged: settings,
|
|
3204
|
+
changed: false
|
|
3205
|
+
};
|
|
3206
|
+
yamlSchemas[SCHEMA_LOCAL_PATH] = SCHEMA_GLOB;
|
|
2860
3207
|
return {
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
3208
|
+
merged: {
|
|
3209
|
+
...settings,
|
|
3210
|
+
"yaml.schemas": yamlSchemas
|
|
3211
|
+
},
|
|
3212
|
+
changed: true
|
|
2865
3213
|
};
|
|
2866
3214
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
3215
|
+
function writeSchemaToSettings(ctx) {
|
|
3216
|
+
if (ctx.exists(SETTINGS_PATH)) {
|
|
3217
|
+
const raw = ctx.read(SETTINGS_PATH);
|
|
3218
|
+
if (!raw) return {
|
|
3219
|
+
filePath: SETTINGS_PATH,
|
|
3220
|
+
action: "skipped",
|
|
3221
|
+
description: "Could not read existing settings"
|
|
3222
|
+
};
|
|
3223
|
+
const parsed = VscodeSettingsSchema.safeParse(parse(raw));
|
|
3224
|
+
if (!parsed.success) return {
|
|
3225
|
+
filePath: SETTINGS_PATH,
|
|
3226
|
+
action: "skipped",
|
|
3227
|
+
description: "Could not parse existing settings"
|
|
3228
|
+
};
|
|
3229
|
+
const { merged, changed } = mergeYamlSchemas(parsed.data);
|
|
3230
|
+
if (!changed) return {
|
|
3231
|
+
filePath: SETTINGS_PATH,
|
|
3232
|
+
action: "skipped",
|
|
3233
|
+
description: "Already has Forgejo schema mapping"
|
|
3234
|
+
};
|
|
3235
|
+
ctx.write(SETTINGS_PATH, serializeSettings(merged));
|
|
3236
|
+
return {
|
|
3237
|
+
filePath: SETTINGS_PATH,
|
|
3238
|
+
action: "updated",
|
|
3239
|
+
description: "Added Forgejo workflow schema mapping"
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
ctx.write(SETTINGS_PATH, serializeSettings({ "yaml.schemas": { [SCHEMA_LOCAL_PATH]: SCHEMA_GLOB } }));
|
|
2872
3243
|
return {
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
3244
|
+
filePath: SETTINGS_PATH,
|
|
3245
|
+
action: "created",
|
|
3246
|
+
description: "Generated .vscode/settings.json with Forgejo workflow schema"
|
|
2876
3247
|
};
|
|
2877
3248
|
}
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
}
|
|
2888
|
-
/** Build the full image reference: namespace/imageName:tag */
|
|
2889
|
-
function imageRef(namespace, imageName, tag) {
|
|
2890
|
-
return `${namespace}/${imageName}:${tag}`;
|
|
2891
|
-
}
|
|
2892
|
-
function log$1(message) {
|
|
2893
|
-
console.log(message);
|
|
2894
|
-
}
|
|
2895
|
-
/** Read the repo name from root package.json. */
|
|
2896
|
-
function readRepoName(executor, cwd) {
|
|
2897
|
-
const rootPkgRaw = executor.readFile(path.join(cwd, "package.json"));
|
|
2898
|
-
if (!rootPkgRaw) throw new FatalError("No package.json found in project root");
|
|
2899
|
-
const repoName = parsePackageJson(rootPkgRaw)?.name;
|
|
2900
|
-
if (!repoName) throw new FatalError("Root package.json must have a name field");
|
|
2901
|
-
return repoName;
|
|
2902
|
-
}
|
|
2903
|
-
/** Build a single docker image from its config. Paths are resolved relative to cwd. */
|
|
2904
|
-
function buildImage(executor, pkg, cwd, extraArgs) {
|
|
2905
|
-
const dockerfilePath = path.resolve(cwd, pkg.docker.dockerfile);
|
|
2906
|
-
const contextPath = path.resolve(cwd, pkg.docker.context);
|
|
2907
|
-
const command = [
|
|
2908
|
-
"docker build",
|
|
2909
|
-
`-f ${dockerfilePath}`,
|
|
2910
|
-
`-t ${pkg.imageName}:latest`,
|
|
2911
|
-
...extraArgs,
|
|
2912
|
-
contextPath
|
|
2913
|
-
].join(" ");
|
|
2914
|
-
executor.execInherit(command);
|
|
2915
|
-
}
|
|
2916
|
-
/**
|
|
2917
|
-
* Detect packages with docker config in .tooling.json and build each one.
|
|
2918
|
-
* Runs `docker build -f <dockerfile> -t <image-name>:latest <context>` for each package.
|
|
2919
|
-
* Dockerfile and context paths are resolved relative to the project root.
|
|
2920
|
-
*
|
|
2921
|
-
* When `packageDir` is set, builds only that single package (for use as an image:build script).
|
|
2922
|
-
*/
|
|
2923
|
-
function runDockerBuild(executor, config) {
|
|
2924
|
-
const repoName = readRepoName(executor, config.cwd);
|
|
2925
|
-
if (config.packageDir) {
|
|
2926
|
-
const pkg = readSinglePackageDocker(executor, config.cwd, config.packageDir, repoName);
|
|
2927
|
-
log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
2928
|
-
buildImage(executor, pkg, config.cwd, config.extraArgs);
|
|
2929
|
-
log$1(`Built ${pkg.imageName}:latest`);
|
|
2930
|
-
return { packages: [pkg] };
|
|
2931
|
-
}
|
|
2932
|
-
const packages = detectDockerPackages(executor, config.cwd, repoName);
|
|
2933
|
-
if (packages.length === 0) {
|
|
2934
|
-
log$1("No packages with docker config found");
|
|
2935
|
-
return { packages: [] };
|
|
3249
|
+
async function generateVscodeSettings(ctx) {
|
|
3250
|
+
const results = [];
|
|
3251
|
+
if (ctx.config.ci !== "forgejo") {
|
|
3252
|
+
results.push({
|
|
3253
|
+
filePath: SETTINGS_PATH,
|
|
3254
|
+
action: "skipped",
|
|
3255
|
+
description: "Not a Forgejo project"
|
|
3256
|
+
});
|
|
3257
|
+
return results;
|
|
2936
3258
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
3259
|
+
const schemaContent = readSchemaFromNodeModules(ctx.targetDir);
|
|
3260
|
+
if (!schemaContent) {
|
|
3261
|
+
results.push({
|
|
3262
|
+
filePath: SCHEMA_LOCAL_PATH,
|
|
3263
|
+
action: "skipped",
|
|
3264
|
+
description: "Could not find @bensandee/config schema in node_modules"
|
|
3265
|
+
});
|
|
3266
|
+
return results;
|
|
2941
3267
|
}
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
* 1. Build all images via runDockerBuild
|
|
2948
|
-
* 2. Login to registry
|
|
2949
|
-
* 3. Tag each image with semver variants from its own package.json version
|
|
2950
|
-
* 4. Push all tags
|
|
2951
|
-
* 5. Logout from registry
|
|
2952
|
-
*/
|
|
2953
|
-
function runDockerPublish(executor, config) {
|
|
2954
|
-
const { packages } = runDockerBuild(executor, {
|
|
2955
|
-
cwd: config.cwd,
|
|
2956
|
-
packageDir: void 0,
|
|
2957
|
-
extraArgs: []
|
|
3268
|
+
const existingSchema = ctx.read(SCHEMA_LOCAL_PATH);
|
|
3269
|
+
if (existingSchema !== void 0 && contentEqual(SCHEMA_LOCAL_PATH, existingSchema, schemaContent)) results.push({
|
|
3270
|
+
filePath: SCHEMA_LOCAL_PATH,
|
|
3271
|
+
action: "skipped",
|
|
3272
|
+
description: "Schema already up to date"
|
|
2958
3273
|
});
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
const loginResult = executor.exec(`echo "${config.password}" | docker login ${config.registryHost} -u ${config.username} --password-stdin`);
|
|
2967
|
-
if (loginResult.exitCode !== 0) throw new FatalError(`Docker login failed: ${loginResult.stderr}`);
|
|
2968
|
-
} else log$1("[dry-run] Skipping docker login");
|
|
2969
|
-
const allTags = [];
|
|
2970
|
-
try {
|
|
2971
|
-
for (const pkg of packages) {
|
|
2972
|
-
const tags = generateTags(pkg.version ?? "");
|
|
2973
|
-
log$1(`${pkg.dir} v${pkg.version} → tags: ${tags.join(", ")}`);
|
|
2974
|
-
for (const tag of tags) {
|
|
2975
|
-
const ref = imageRef(config.registryNamespace, pkg.imageName, tag);
|
|
2976
|
-
allTags.push(ref);
|
|
2977
|
-
log$1(`Tagging ${pkg.imageName} → ${ref}`);
|
|
2978
|
-
const tagResult = executor.exec(`docker tag ${pkg.imageName} ${ref}`);
|
|
2979
|
-
if (tagResult.exitCode !== 0) throw new FatalError(`docker tag failed: ${tagResult.stderr}`);
|
|
2980
|
-
if (!config.dryRun) {
|
|
2981
|
-
log$1(`Pushing ${ref}...`);
|
|
2982
|
-
const pushResult = executor.exec(`docker push ${ref}`);
|
|
2983
|
-
if (pushResult.exitCode !== 0) throw new FatalError(`docker push failed: ${pushResult.stderr}`);
|
|
2984
|
-
} else log$1(`[dry-run] Skipping push for ${ref}`);
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
} finally {
|
|
2988
|
-
if (!config.dryRun) {
|
|
2989
|
-
log$1(`Logging out from ${config.registryHost}...`);
|
|
2990
|
-
executor.exec(`docker logout ${config.registryHost}`);
|
|
2991
|
-
}
|
|
3274
|
+
else {
|
|
3275
|
+
ctx.write(SCHEMA_LOCAL_PATH, schemaContent);
|
|
3276
|
+
results.push({
|
|
3277
|
+
filePath: SCHEMA_LOCAL_PATH,
|
|
3278
|
+
action: existingSchema ? "updated" : "created",
|
|
3279
|
+
description: "Copied Forgejo workflow schema from @bensandee/config"
|
|
3280
|
+
});
|
|
2992
3281
|
}
|
|
2993
|
-
|
|
2994
|
-
return
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3282
|
+
results.push(writeSchemaToSettings(ctx));
|
|
3283
|
+
return results;
|
|
3284
|
+
}
|
|
3285
|
+
//#endregion
|
|
3286
|
+
//#region src/generators/pipeline.ts
|
|
3287
|
+
/** Run all generators sequentially and return their results. */
|
|
3288
|
+
async function runGenerators(ctx) {
|
|
3289
|
+
const results = [];
|
|
3290
|
+
results.push(await generatePackageJson(ctx));
|
|
3291
|
+
results.push(await generatePnpmWorkspace(ctx));
|
|
3292
|
+
results.push(...await generateTsconfig(ctx));
|
|
3293
|
+
results.push(await generateTsdown(ctx));
|
|
3294
|
+
results.push(await generateOxlint(ctx));
|
|
3295
|
+
results.push(await generateFormatter(ctx));
|
|
3296
|
+
results.push(...await generateLefthook(ctx));
|
|
3297
|
+
results.push(await generateGitignore(ctx));
|
|
3298
|
+
results.push(await generateKnip(ctx));
|
|
3299
|
+
results.push(await generateRenovate(ctx));
|
|
3300
|
+
results.push(await generateCi(ctx));
|
|
3301
|
+
results.push(...await generateClaudeSettings(ctx));
|
|
3302
|
+
results.push(await generateReleaseIt(ctx));
|
|
3303
|
+
results.push(await generateChangesets(ctx));
|
|
3304
|
+
results.push(await generateReleaseCi(ctx));
|
|
3305
|
+
results.push(await generateDeployCi(ctx));
|
|
3306
|
+
results.push(...await generateVitest(ctx));
|
|
3307
|
+
results.push(...await generateVscodeSettings(ctx));
|
|
3308
|
+
results.push(saveToolingConfig(ctx, ctx.config));
|
|
3309
|
+
return results;
|
|
2998
3310
|
}
|
|
2999
3311
|
//#endregion
|
|
3000
3312
|
//#region src/generators/migrate-prompt.ts
|
|
@@ -3007,7 +3319,7 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
3007
3319
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3008
3320
|
sections.push("# Migration Prompt");
|
|
3009
3321
|
sections.push("");
|
|
3010
|
-
sections.push(`_Generated by \`@bensandee/tooling@0.
|
|
3322
|
+
sections.push(`_Generated by \`@bensandee/tooling@0.29.0 repo:sync\` on ${timestamp}_`);
|
|
3011
3323
|
sections.push("");
|
|
3012
3324
|
sections.push("The following prompt was generated by `@bensandee/tooling repo:sync`. Paste it into Claude Code or another AI assistant to finish migrating this repository.");
|
|
3013
3325
|
sections.push("");
|
|
@@ -3041,29 +3353,6 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
3041
3353
|
}
|
|
3042
3354
|
sections.push("## Migration tasks");
|
|
3043
3355
|
sections.push("");
|
|
3044
|
-
if (config.releaseStrategy === "simple" && !detected.hasCommitAndTagVersion) {
|
|
3045
|
-
sections.push("### Add commit-and-tag-version to devDependencies");
|
|
3046
|
-
sections.push("");
|
|
3047
|
-
sections.push("The `simple` release strategy requires `commit-and-tag-version` as a root devDependency so that `pnpm exec commit-and-tag-version` resolves correctly.");
|
|
3048
|
-
sections.push("");
|
|
3049
|
-
sections.push("Run:");
|
|
3050
|
-
sections.push("");
|
|
3051
|
-
sections.push("```sh");
|
|
3052
|
-
sections.push("pnpm add -D -w commit-and-tag-version");
|
|
3053
|
-
sections.push("```");
|
|
3054
|
-
sections.push("");
|
|
3055
|
-
}
|
|
3056
|
-
if (config.releaseStrategy !== "none" && !detected.hasRepositoryField) {
|
|
3057
|
-
sections.push("### Add repository field to package.json");
|
|
3058
|
-
sections.push("");
|
|
3059
|
-
sections.push(`The release strategy \`${config.releaseStrategy}\` requires a \`repository\` field in \`package.json\` so that \`release:trigger\` can determine the correct hosting platform (Forgejo vs GitHub).`);
|
|
3060
|
-
sections.push("");
|
|
3061
|
-
sections.push("Add the appropriate repository URL to `package.json`:");
|
|
3062
|
-
sections.push("");
|
|
3063
|
-
sections.push("- For Forgejo: `\"repository\": \"https://your-forgejo-instance.com/owner/repo\"`");
|
|
3064
|
-
sections.push("- For GitHub: `\"repository\": \"https://github.com/owner/repo\"`");
|
|
3065
|
-
sections.push("");
|
|
3066
|
-
}
|
|
3067
3356
|
const legacyToRemove = detected.legacyConfigs.filter((legacy) => !(legacy.tool === "prettier" && config.formatter === "prettier"));
|
|
3068
3357
|
if (legacyToRemove.length > 0) {
|
|
3069
3358
|
sections.push("### Remove legacy tooling");
|
|
@@ -4820,7 +5109,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
4820
5109
|
const main = defineCommand({
|
|
4821
5110
|
meta: {
|
|
4822
5111
|
name: "bst",
|
|
4823
|
-
version: "0.
|
|
5112
|
+
version: "0.29.0",
|
|
4824
5113
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
4825
5114
|
},
|
|
4826
5115
|
subCommands: {
|
|
@@ -4836,7 +5125,7 @@ const main = defineCommand({
|
|
|
4836
5125
|
"docker:check": dockerCheckCommand
|
|
4837
5126
|
}
|
|
4838
5127
|
});
|
|
4839
|
-
console.log(`@bensandee/tooling v0.
|
|
5128
|
+
console.log(`@bensandee/tooling v0.29.0`);
|
|
4840
5129
|
async function run() {
|
|
4841
5130
|
await runMain(main);
|
|
4842
5131
|
process.exit(process.exitCode ?? 0);
|