@bensandee/tooling 0.23.0 → 0.25.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 +471 -388
- package/dist/index.d.mts +5 -0
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const LEGACY_TOOLS = [
|
|
|
22
22
|
//#endregion
|
|
23
23
|
//#region src/utils/json.ts
|
|
24
24
|
const StringRecord = z.record(z.string(), z.string());
|
|
25
|
-
const PackageJsonSchema = z.
|
|
25
|
+
const PackageJsonSchema = z.looseObject({
|
|
26
26
|
name: z.string().optional(),
|
|
27
27
|
version: z.string().optional(),
|
|
28
28
|
private: z.boolean().optional(),
|
|
@@ -35,20 +35,21 @@ const PackageJsonSchema = z.object({
|
|
|
35
35
|
main: z.string().optional(),
|
|
36
36
|
types: z.string().optional(),
|
|
37
37
|
typings: z.string().optional(),
|
|
38
|
-
engines: StringRecord.optional()
|
|
39
|
-
}).
|
|
40
|
-
|
|
38
|
+
engines: StringRecord.optional(),
|
|
39
|
+
repository: z.union([z.string(), z.looseObject({ url: z.string() })]).optional()
|
|
40
|
+
});
|
|
41
|
+
const TsconfigSchema = z.looseObject({
|
|
41
42
|
extends: z.union([z.string(), z.array(z.string())]).optional(),
|
|
42
43
|
include: z.array(z.string()).optional(),
|
|
43
44
|
exclude: z.array(z.string()).optional(),
|
|
44
45
|
files: z.array(z.string()).optional(),
|
|
45
|
-
references: z.array(z.
|
|
46
|
+
references: z.array(z.looseObject({ path: z.string() })).optional(),
|
|
46
47
|
compilerOptions: z.record(z.string(), z.unknown()).optional()
|
|
47
|
-
})
|
|
48
|
-
const RenovateSchema = z.
|
|
48
|
+
});
|
|
49
|
+
const RenovateSchema = z.looseObject({
|
|
49
50
|
$schema: z.string().optional(),
|
|
50
51
|
extends: z.array(z.string()).optional()
|
|
51
|
-
})
|
|
52
|
+
});
|
|
52
53
|
/** Parse a JSONC string as a tsconfig.json. Returns a typed object with `{}` fallback on failure. */
|
|
53
54
|
function parseTsconfig(raw) {
|
|
54
55
|
const result = TsconfigSchema.safeParse(parse(raw));
|
|
@@ -63,7 +64,7 @@ function parseRenovateJson(raw) {
|
|
|
63
64
|
return {};
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
|
-
const ChangesetConfigSchema = z.
|
|
67
|
+
const ChangesetConfigSchema = z.looseObject({ commit: z.union([z.boolean(), z.string()]).optional() });
|
|
67
68
|
/** Parse a JSON string as a .changeset/config.json. Returns `undefined` on failure. */
|
|
68
69
|
function parseChangesetConfig(raw) {
|
|
69
70
|
try {
|
|
@@ -108,6 +109,7 @@ function detectProject(targetDir) {
|
|
|
108
109
|
hasReleaseItConfig: exists(".release-it.json") || exists(".release-it.yaml") || exists(".release-it.toml"),
|
|
109
110
|
hasSimpleReleaseConfig: exists(".versionrc") || exists(".versionrc.json") || exists(".versionrc.js"),
|
|
110
111
|
hasChangesetsConfig: exists(".changeset/config.json"),
|
|
112
|
+
hasRepositoryField: !!readPackageJson(targetDir)?.repository,
|
|
111
113
|
legacyConfigs: detectLegacyConfigs(targetDir)
|
|
112
114
|
};
|
|
113
115
|
}
|
|
@@ -981,7 +983,7 @@ function getAddedDevDepNames(config) {
|
|
|
981
983
|
const deps = { ...ROOT_DEV_DEPS };
|
|
982
984
|
if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
|
|
983
985
|
deps["@bensandee/config"] = "0.8.2";
|
|
984
|
-
deps["@bensandee/tooling"] = "0.
|
|
986
|
+
deps["@bensandee/tooling"] = "0.25.0";
|
|
985
987
|
if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
|
|
986
988
|
if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
|
|
987
989
|
addReleaseDeps(deps, config);
|
|
@@ -1006,7 +1008,7 @@ async function generatePackageJson(ctx) {
|
|
|
1006
1008
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1007
1009
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1008
1010
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.8.2";
|
|
1009
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1011
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.25.0";
|
|
1010
1012
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
|
|
1011
1013
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
|
|
1012
1014
|
if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
|
|
@@ -1894,7 +1896,8 @@ function buildSettings(ctx) {
|
|
|
1894
1896
|
instructions: [
|
|
1895
1897
|
"Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
|
|
1896
1898
|
"No typecasts (as/any). Use zod schemas, type guards, or narrowing instead.",
|
|
1897
|
-
"Fix lint violations instead of suppressing them. Only add disable comments when suppression is genuinely the best option."
|
|
1899
|
+
"Fix lint violations instead of suppressing them. Only add disable comments when suppression is genuinely the best option.",
|
|
1900
|
+
"Prefer extensionless imports; if an extension is required, use .ts over .js."
|
|
1898
1901
|
],
|
|
1899
1902
|
enabledPlugins,
|
|
1900
1903
|
extraKnownMarketplaces
|
|
@@ -2475,7 +2478,7 @@ const SCHEMA_NPM_PATH = "@bensandee/config/schemas/forgejo-workflow.schema.json"
|
|
|
2475
2478
|
const SCHEMA_LOCAL_PATH = ".vscode/forgejo-workflow.schema.json";
|
|
2476
2479
|
const SETTINGS_PATH = ".vscode/settings.json";
|
|
2477
2480
|
const SCHEMA_GLOB = ".forgejo/workflows/*.{yml,yaml}";
|
|
2478
|
-
const VscodeSettingsSchema = z.
|
|
2481
|
+
const VscodeSettingsSchema = z.looseObject({ "yaml.schemas": z.record(z.string(), z.unknown()).default({}) });
|
|
2479
2482
|
function readSchemaFromNodeModules(targetDir) {
|
|
2480
2483
|
const candidate = path.join(targetDir, "node_modules", SCHEMA_NPM_PATH);
|
|
2481
2484
|
if (!existsSync(candidate)) return void 0;
|
|
@@ -2599,54 +2602,349 @@ async function runGenerators(ctx) {
|
|
|
2599
2602
|
return results;
|
|
2600
2603
|
}
|
|
2601
2604
|
//#endregion
|
|
2602
|
-
//#region src/
|
|
2605
|
+
//#region src/release/docker.ts
|
|
2606
|
+
const ToolingDockerMapSchema = z.record(z.string(), z.object({
|
|
2607
|
+
dockerfile: z.string(),
|
|
2608
|
+
context: z.string().default(".")
|
|
2609
|
+
}));
|
|
2610
|
+
const ToolingConfigDockerSchema = z.object({ docker: ToolingDockerMapSchema.optional() });
|
|
2611
|
+
const PackageInfoSchema = z.object({
|
|
2612
|
+
name: z.string().optional(),
|
|
2613
|
+
version: z.string().optional()
|
|
2614
|
+
});
|
|
2615
|
+
/** Read the docker map from .tooling.json. Returns empty record if missing or invalid. */
|
|
2616
|
+
function loadDockerMap(executor, cwd) {
|
|
2617
|
+
const configPath = path.join(cwd, ".tooling.json");
|
|
2618
|
+
const raw = executor.readFile(configPath);
|
|
2619
|
+
if (!raw) return {};
|
|
2620
|
+
try {
|
|
2621
|
+
const result = ToolingConfigDockerSchema.safeParse(JSON.parse(raw));
|
|
2622
|
+
if (!result.success || !result.data.docker) return {};
|
|
2623
|
+
return result.data.docker;
|
|
2624
|
+
} catch (_error) {
|
|
2625
|
+
return {};
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
/** Read name and version from a package's package.json. */
|
|
2629
|
+
function readPackageInfo(executor, packageJsonPath) {
|
|
2630
|
+
const raw = executor.readFile(packageJsonPath);
|
|
2631
|
+
if (!raw) return {
|
|
2632
|
+
name: void 0,
|
|
2633
|
+
version: void 0
|
|
2634
|
+
};
|
|
2635
|
+
try {
|
|
2636
|
+
const result = PackageInfoSchema.safeParse(JSON.parse(raw));
|
|
2637
|
+
if (!result.success) return {
|
|
2638
|
+
name: void 0,
|
|
2639
|
+
version: void 0
|
|
2640
|
+
};
|
|
2641
|
+
return {
|
|
2642
|
+
name: result.data.name,
|
|
2643
|
+
version: result.data.version
|
|
2644
|
+
};
|
|
2645
|
+
} catch (_error) {
|
|
2646
|
+
return {
|
|
2647
|
+
name: void 0,
|
|
2648
|
+
version: void 0
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
/** Convention paths to check for Dockerfiles in a package directory. */
|
|
2653
|
+
const CONVENTION_DOCKERFILE_PATHS = ["Dockerfile", "docker/Dockerfile"];
|
|
2603
2654
|
/**
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2655
|
+
* Find a Dockerfile at convention paths for a monorepo package.
|
|
2656
|
+
* Checks packages/{dir}/Dockerfile and packages/{dir}/docker/Dockerfile.
|
|
2606
2657
|
*/
|
|
2607
|
-
function
|
|
2608
|
-
const
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
sections.push("");
|
|
2615
|
-
sections.push("## What was changed");
|
|
2616
|
-
sections.push("");
|
|
2617
|
-
const created = results.filter((r) => r.action === "created");
|
|
2618
|
-
const updated = results.filter((r) => r.action === "updated");
|
|
2619
|
-
const skipped = results.filter((r) => r.action === "skipped");
|
|
2620
|
-
const archived = results.filter((r) => r.action === "archived");
|
|
2621
|
-
if (created.length > 0) {
|
|
2622
|
-
sections.push("**Created:**");
|
|
2623
|
-
for (const r of created) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
2624
|
-
sections.push("");
|
|
2658
|
+
function findConventionDockerfile(executor, cwd, dir) {
|
|
2659
|
+
for (const rel of CONVENTION_DOCKERFILE_PATHS) {
|
|
2660
|
+
const dockerfilePath = `packages/${dir}/${rel}`;
|
|
2661
|
+
if (executor.readFile(path.join(cwd, dockerfilePath)) !== null) return {
|
|
2662
|
+
dockerfile: dockerfilePath,
|
|
2663
|
+
context: "."
|
|
2664
|
+
};
|
|
2625
2665
|
}
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Find a Dockerfile at convention paths for a single-package repo.
|
|
2669
|
+
* Checks Dockerfile and docker/Dockerfile at the project root.
|
|
2670
|
+
*/
|
|
2671
|
+
function findRootDockerfile(executor, cwd) {
|
|
2672
|
+
for (const rel of CONVENTION_DOCKERFILE_PATHS) if (executor.readFile(path.join(cwd, rel)) !== null) return {
|
|
2673
|
+
dockerfile: rel,
|
|
2674
|
+
context: "."
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* Discover Docker packages by convention and merge with .tooling.json overrides.
|
|
2679
|
+
*
|
|
2680
|
+
* Convention: any package with a Dockerfile or docker/Dockerfile is a Docker package.
|
|
2681
|
+
* For monorepos, scans packages/{name}/. For single-package repos, scans the root.
|
|
2682
|
+
* The docker map in .tooling.json overrides convention-discovered config and can add
|
|
2683
|
+
* packages at non-standard locations.
|
|
2684
|
+
*
|
|
2685
|
+
* Image names are derived from {root-name}-{package-name} using each package's package.json name.
|
|
2686
|
+
* Versions are read from each package's own package.json.
|
|
2687
|
+
*/
|
|
2688
|
+
function detectDockerPackages(executor, cwd, repoName) {
|
|
2689
|
+
const overrides = loadDockerMap(executor, cwd);
|
|
2690
|
+
const packageDirs = executor.listPackageDirs(cwd);
|
|
2691
|
+
const packages = [];
|
|
2692
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2693
|
+
if (packageDirs.length > 0) {
|
|
2694
|
+
for (const dir of packageDirs) {
|
|
2695
|
+
const convention = findConventionDockerfile(executor, cwd, dir);
|
|
2696
|
+
const docker = overrides[dir] ?? convention;
|
|
2697
|
+
if (docker) {
|
|
2698
|
+
const { name, version } = readPackageInfo(executor, path.join(cwd, "packages", dir, "package.json"));
|
|
2699
|
+
packages.push({
|
|
2700
|
+
dir,
|
|
2701
|
+
imageName: `${repoName}-${name ?? dir}`,
|
|
2702
|
+
version,
|
|
2703
|
+
docker
|
|
2704
|
+
});
|
|
2705
|
+
seen.add(dir);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
for (const [dir, docker] of Object.entries(overrides)) if (!seen.has(dir)) {
|
|
2709
|
+
const { name, version } = readPackageInfo(executor, path.join(cwd, "packages", dir, "package.json"));
|
|
2710
|
+
packages.push({
|
|
2711
|
+
dir,
|
|
2712
|
+
imageName: `${repoName}-${name ?? dir}`,
|
|
2713
|
+
version,
|
|
2714
|
+
docker
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
} else {
|
|
2718
|
+
const convention = findRootDockerfile(executor, cwd);
|
|
2719
|
+
const docker = overrides["."] ?? convention;
|
|
2720
|
+
if (docker) {
|
|
2721
|
+
const { name, version } = readPackageInfo(executor, path.join(cwd, "package.json"));
|
|
2722
|
+
packages.push({
|
|
2723
|
+
dir: ".",
|
|
2724
|
+
imageName: name ?? repoName,
|
|
2725
|
+
version,
|
|
2726
|
+
docker
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2630
2729
|
}
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2730
|
+
return packages;
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Read docker config for a single package, checking convention paths first,
|
|
2734
|
+
* then .tooling.json overrides. Used by the per-package image:build script.
|
|
2735
|
+
*/
|
|
2736
|
+
function readSinglePackageDocker(executor, cwd, packageDir, repoName) {
|
|
2737
|
+
const dir = path.basename(path.resolve(cwd, packageDir));
|
|
2738
|
+
const convention = findConventionDockerfile(executor, cwd, dir);
|
|
2739
|
+
const docker = loadDockerMap(executor, cwd)[dir] ?? convention;
|
|
2740
|
+
if (!docker) throw new FatalError(`No Dockerfile found for package "${dir}" (checked convention paths and .tooling.json)`);
|
|
2741
|
+
const { name, version } = readPackageInfo(executor, path.join(cwd, "packages", dir, "package.json"));
|
|
2742
|
+
return {
|
|
2743
|
+
dir,
|
|
2744
|
+
imageName: `${repoName}-${name ?? dir}`,
|
|
2745
|
+
version,
|
|
2746
|
+
docker
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
/** Parse semver version string into major, minor, patch components. */
|
|
2750
|
+
function parseSemver(version) {
|
|
2751
|
+
const clean = version.replace(/^v/, "");
|
|
2752
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(clean);
|
|
2753
|
+
if (!match?.[1] || !match[2] || !match[3]) throw new FatalError(`Invalid semver version: ${version}`);
|
|
2754
|
+
return {
|
|
2755
|
+
major: Number(match[1]),
|
|
2756
|
+
minor: Number(match[2]),
|
|
2757
|
+
patch: Number(match[3])
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
/** Generate semver tag variants: latest, vX.Y.Z, vX.Y, vX */
|
|
2761
|
+
function generateTags(version) {
|
|
2762
|
+
const { major, minor, patch } = parseSemver(version);
|
|
2763
|
+
return [
|
|
2764
|
+
"latest",
|
|
2765
|
+
`v${major}.${minor}.${patch}`,
|
|
2766
|
+
`v${major}.${minor}`,
|
|
2767
|
+
`v${major}`
|
|
2768
|
+
];
|
|
2769
|
+
}
|
|
2770
|
+
/** Build the full image reference: namespace/imageName:tag */
|
|
2771
|
+
function imageRef(namespace, imageName, tag) {
|
|
2772
|
+
return `${namespace}/${imageName}:${tag}`;
|
|
2773
|
+
}
|
|
2774
|
+
function log$1(message) {
|
|
2775
|
+
console.log(message);
|
|
2776
|
+
}
|
|
2777
|
+
function debug$1(verbose, message) {
|
|
2778
|
+
if (verbose) console.log(`[debug] ${message}`);
|
|
2779
|
+
}
|
|
2780
|
+
/** Read the repo name from root package.json. */
|
|
2781
|
+
function readRepoName(executor, cwd) {
|
|
2782
|
+
const rootPkgRaw = executor.readFile(path.join(cwd, "package.json"));
|
|
2783
|
+
if (!rootPkgRaw) throw new FatalError("No package.json found in project root");
|
|
2784
|
+
const repoName = parsePackageJson(rootPkgRaw)?.name;
|
|
2785
|
+
if (!repoName) throw new FatalError("Root package.json must have a name field");
|
|
2786
|
+
return repoName;
|
|
2787
|
+
}
|
|
2788
|
+
/** Build a single docker image from its config. Paths are resolved relative to cwd. */
|
|
2789
|
+
function buildImage(executor, pkg, cwd, verbose, extraArgs) {
|
|
2790
|
+
const dockerfilePath = path.resolve(cwd, pkg.docker.dockerfile);
|
|
2791
|
+
const contextPath = path.resolve(cwd, pkg.docker.context);
|
|
2792
|
+
const command = [
|
|
2793
|
+
"docker build",
|
|
2794
|
+
`-f ${dockerfilePath}`,
|
|
2795
|
+
`-t ${pkg.imageName}:latest`,
|
|
2796
|
+
...extraArgs,
|
|
2797
|
+
contextPath
|
|
2798
|
+
].join(" ");
|
|
2799
|
+
debug$1(verbose, `Running: ${command}`);
|
|
2800
|
+
const buildResult = executor.exec(command);
|
|
2801
|
+
debug$1(verbose, `Build stdout: ${buildResult.stdout}`);
|
|
2802
|
+
if (buildResult.exitCode !== 0) throw new FatalError(`docker build failed for ${pkg.dir} (exit ${buildResult.exitCode}): ${buildResult.stderr}`);
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* Detect packages with docker config in .tooling.json and build each one.
|
|
2806
|
+
* Runs `docker build -f <dockerfile> -t <image-name>:latest <context>` for each package.
|
|
2807
|
+
* Dockerfile and context paths are resolved relative to the project root.
|
|
2808
|
+
*
|
|
2809
|
+
* When `packageDir` is set, builds only that single package (for use as an image:build script).
|
|
2810
|
+
*/
|
|
2811
|
+
function runDockerBuild(executor, config) {
|
|
2812
|
+
const repoName = readRepoName(executor, config.cwd);
|
|
2813
|
+
if (config.packageDir) {
|
|
2814
|
+
const pkg = readSinglePackageDocker(executor, config.cwd, config.packageDir, repoName);
|
|
2815
|
+
log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
2816
|
+
buildImage(executor, pkg, config.cwd, config.verbose, config.extraArgs);
|
|
2817
|
+
log$1(`Built ${pkg.imageName}:latest`);
|
|
2818
|
+
return { packages: [pkg] };
|
|
2635
2819
|
}
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2820
|
+
const packages = detectDockerPackages(executor, config.cwd, repoName);
|
|
2821
|
+
if (packages.length === 0) {
|
|
2822
|
+
log$1("No packages with docker config found");
|
|
2823
|
+
return { packages: [] };
|
|
2640
2824
|
}
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2825
|
+
log$1(`Found ${packages.length} Docker package(s): ${packages.map((p) => p.dir).join(", ")}`);
|
|
2826
|
+
for (const pkg of packages) {
|
|
2827
|
+
log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
2828
|
+
buildImage(executor, pkg, config.cwd, config.verbose, config.extraArgs);
|
|
2829
|
+
}
|
|
2830
|
+
log$1(`Built ${packages.length} image(s)`);
|
|
2831
|
+
return { packages };
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Run the full Docker publish pipeline:
|
|
2835
|
+
* 1. Build all images via runDockerBuild
|
|
2836
|
+
* 2. Login to registry
|
|
2837
|
+
* 3. Tag each image with semver variants from its own package.json version
|
|
2838
|
+
* 4. Push all tags
|
|
2839
|
+
* 5. Logout from registry
|
|
2840
|
+
*/
|
|
2841
|
+
function runDockerPublish(executor, config) {
|
|
2842
|
+
const { packages } = runDockerBuild(executor, {
|
|
2843
|
+
cwd: config.cwd,
|
|
2844
|
+
packageDir: void 0,
|
|
2845
|
+
verbose: config.verbose,
|
|
2846
|
+
extraArgs: []
|
|
2847
|
+
});
|
|
2848
|
+
if (packages.length === 0) return {
|
|
2849
|
+
packages: [],
|
|
2850
|
+
tags: []
|
|
2851
|
+
};
|
|
2852
|
+
for (const pkg of packages) if (!pkg.version) throw new FatalError(`Package ${pkg.dir} has docker config but no version in package.json`);
|
|
2853
|
+
if (!config.dryRun) {
|
|
2854
|
+
log$1(`Logging in to ${config.registryHost}...`);
|
|
2855
|
+
const loginResult = executor.exec(`echo "${config.password}" | docker login ${config.registryHost} -u ${config.username} --password-stdin`);
|
|
2856
|
+
if (loginResult.exitCode !== 0) throw new FatalError(`Docker login failed: ${loginResult.stderr}`);
|
|
2857
|
+
} else log$1("[dry-run] Skipping docker login");
|
|
2858
|
+
const allTags = [];
|
|
2859
|
+
try {
|
|
2860
|
+
for (const pkg of packages) {
|
|
2861
|
+
const tags = generateTags(pkg.version ?? "");
|
|
2862
|
+
log$1(`${pkg.dir} v${pkg.version} → tags: ${tags.join(", ")}`);
|
|
2863
|
+
for (const tag of tags) {
|
|
2864
|
+
const ref = imageRef(config.registryNamespace, pkg.imageName, tag);
|
|
2865
|
+
allTags.push(ref);
|
|
2866
|
+
log$1(`Tagging ${pkg.imageName} → ${ref}`);
|
|
2867
|
+
const tagResult = executor.exec(`docker tag ${pkg.imageName} ${ref}`);
|
|
2868
|
+
if (tagResult.exitCode !== 0) throw new FatalError(`docker tag failed: ${tagResult.stderr}`);
|
|
2869
|
+
if (!config.dryRun) {
|
|
2870
|
+
log$1(`Pushing ${ref}...`);
|
|
2871
|
+
const pushResult = executor.exec(`docker push ${ref}`);
|
|
2872
|
+
if (pushResult.exitCode !== 0) throw new FatalError(`docker push failed: ${pushResult.stderr}`);
|
|
2873
|
+
} else log$1(`[dry-run] Skipping push for ${ref}`);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
} finally {
|
|
2877
|
+
if (!config.dryRun) {
|
|
2878
|
+
log$1(`Logging out from ${config.registryHost}...`);
|
|
2879
|
+
executor.exec(`docker logout ${config.registryHost}`);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
log$1(`Published ${allTags.length} image tag(s)`);
|
|
2883
|
+
return {
|
|
2884
|
+
packages,
|
|
2885
|
+
tags: allTags
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
//#endregion
|
|
2889
|
+
//#region src/generators/migrate-prompt.ts
|
|
2890
|
+
/**
|
|
2891
|
+
* Generate a context-aware AI migration prompt based on what the CLI did.
|
|
2892
|
+
* This prompt can be pasted into Claude Code (or similar) to finish the migration.
|
|
2893
|
+
*/
|
|
2894
|
+
function generateMigratePrompt(results, config, detected) {
|
|
2895
|
+
const sections = [];
|
|
2896
|
+
sections.push("# Migration Prompt");
|
|
2897
|
+
sections.push("");
|
|
2898
|
+
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.");
|
|
2899
|
+
sections.push("");
|
|
2900
|
+
sections.push("> **Tip:** Before starting, run `/init` in Claude Code to generate a `CLAUDE.md` that gives the AI a complete picture of your repository's structure, conventions, and build commands.");
|
|
2901
|
+
sections.push("");
|
|
2902
|
+
sections.push("## What was changed");
|
|
2903
|
+
sections.push("");
|
|
2904
|
+
const created = results.filter((r) => r.action === "created");
|
|
2905
|
+
const updated = results.filter((r) => r.action === "updated");
|
|
2906
|
+
const skipped = results.filter((r) => r.action === "skipped");
|
|
2907
|
+
const archived = results.filter((r) => r.action === "archived");
|
|
2908
|
+
if (created.length > 0) {
|
|
2909
|
+
sections.push("**Created:**");
|
|
2910
|
+
for (const r of created) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
2911
|
+
sections.push("");
|
|
2912
|
+
}
|
|
2913
|
+
if (updated.length > 0) {
|
|
2914
|
+
sections.push("**Updated:**");
|
|
2915
|
+
for (const r of updated) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
2916
|
+
sections.push("");
|
|
2917
|
+
}
|
|
2918
|
+
if (archived.length > 0) {
|
|
2919
|
+
sections.push("**Archived:**");
|
|
2920
|
+
for (const r of archived) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
2921
|
+
sections.push("");
|
|
2922
|
+
}
|
|
2923
|
+
if (skipped.length > 0) {
|
|
2924
|
+
sections.push("**Skipped (review these):**");
|
|
2925
|
+
for (const r of skipped) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
2926
|
+
sections.push("");
|
|
2927
|
+
}
|
|
2928
|
+
sections.push("## Migration tasks");
|
|
2929
|
+
sections.push("");
|
|
2930
|
+
if (config.releaseStrategy !== "none" && !detected.hasRepositoryField) {
|
|
2931
|
+
sections.push("### Add repository field to package.json");
|
|
2932
|
+
sections.push("");
|
|
2933
|
+
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).`);
|
|
2934
|
+
sections.push("");
|
|
2935
|
+
sections.push("Add the appropriate repository URL to `package.json`:");
|
|
2936
|
+
sections.push("");
|
|
2937
|
+
sections.push("- For Forgejo: `\"repository\": \"https://your-forgejo-instance.com/owner/repo\"`");
|
|
2938
|
+
sections.push("- For GitHub: `\"repository\": \"https://github.com/owner/repo\"`");
|
|
2939
|
+
sections.push("");
|
|
2940
|
+
}
|
|
2941
|
+
const legacyToRemove = detected.legacyConfigs.filter((legacy) => !(legacy.tool === "prettier" && config.formatter === "prettier"));
|
|
2942
|
+
if (legacyToRemove.length > 0) {
|
|
2943
|
+
sections.push("### Remove legacy tooling");
|
|
2944
|
+
sections.push("");
|
|
2945
|
+
for (const legacy of legacyToRemove) {
|
|
2946
|
+
const replacement = {
|
|
2947
|
+
eslint: "oxlint",
|
|
2650
2948
|
prettier: "oxfmt",
|
|
2651
2949
|
jest: "vitest",
|
|
2652
2950
|
webpack: "tsdown",
|
|
@@ -2769,15 +3067,29 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
2769
3067
|
}
|
|
2770
3068
|
//#endregion
|
|
2771
3069
|
//#region src/commands/repo-init.ts
|
|
3070
|
+
/** Adapt a GeneratorContext to the DockerFileReader interface used by detectDockerPackages. */
|
|
3071
|
+
function contextAsDockerReader(ctx) {
|
|
3072
|
+
return {
|
|
3073
|
+
listPackageDirs(cwd) {
|
|
3074
|
+
const packagesDir = path.join(cwd, "packages");
|
|
3075
|
+
try {
|
|
3076
|
+
return readdirSync(packagesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3077
|
+
} catch (_error) {
|
|
3078
|
+
return [];
|
|
3079
|
+
}
|
|
3080
|
+
},
|
|
3081
|
+
readFile(filePath) {
|
|
3082
|
+
const rel = path.relative(ctx.targetDir, filePath);
|
|
3083
|
+
return ctx.read(rel) ?? null;
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
2772
3087
|
/** Log what was detected so the user understands generator decisions. */
|
|
2773
3088
|
function logDetectionSummary(ctx) {
|
|
2774
|
-
const
|
|
2775
|
-
if (
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
if (publishable.length > 0) p.log.info(`Will publish npm packages: ${publishable.map((pkg) => pkg.name).join(", ")}`);
|
|
2779
|
-
else p.log.info("No publishable npm packages — npm registry setup will be skipped");
|
|
2780
|
-
}
|
|
3089
|
+
const dockerPackages = detectDockerPackages(contextAsDockerReader(ctx), ctx.targetDir, ctx.config.name);
|
|
3090
|
+
if (dockerPackages.length > 0) p.log.info(`Docker images: ${dockerPackages.map((pkg) => pkg.imageName).join(", ")}`);
|
|
3091
|
+
const publishable = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson);
|
|
3092
|
+
if (publishable.length > 0) p.log.info(`npm packages: ${publishable.map((pkg) => pkg.name).join(", ")}`);
|
|
2781
3093
|
}
|
|
2782
3094
|
async function runInit(config, options = {}) {
|
|
2783
3095
|
const detected = detectProject(config.targetDir);
|
|
@@ -2798,9 +3110,16 @@ async function runInit(config, options = {}) {
|
|
|
2798
3110
|
if (p.isCancel(result)) return "skip";
|
|
2799
3111
|
return result;
|
|
2800
3112
|
}));
|
|
3113
|
+
if (config.releaseStrategy !== "none" && !ctx.packageJson?.repository) p.log.warn(`package.json is missing a "repository" field — required for release strategy "${config.releaseStrategy}"`);
|
|
2801
3114
|
logDetectionSummary(ctx);
|
|
2802
3115
|
s.start("Generating configuration files...");
|
|
2803
|
-
|
|
3116
|
+
let results;
|
|
3117
|
+
try {
|
|
3118
|
+
results = await runGenerators(ctx);
|
|
3119
|
+
} catch (error) {
|
|
3120
|
+
s.stop("Generation failed!");
|
|
3121
|
+
throw error;
|
|
3122
|
+
}
|
|
2804
3123
|
const alreadyArchived = new Set(results.filter((r) => r.action === "archived").map((r) => r.filePath));
|
|
2805
3124
|
for (const rel of archivedFiles) if (!alreadyArchived.has(rel)) results.push({
|
|
2806
3125
|
filePath: rel,
|
|
@@ -2817,7 +3136,8 @@ async function runInit(config, options = {}) {
|
|
|
2817
3136
|
if (results.some((r) => r.action === "archived" && r.filePath.startsWith(".husky/"))) try {
|
|
2818
3137
|
execSync("git config --unset core.hooksPath", {
|
|
2819
3138
|
cwd: config.targetDir,
|
|
2820
|
-
stdio: "ignore"
|
|
3139
|
+
stdio: "ignore",
|
|
3140
|
+
timeout: 5e3
|
|
2821
3141
|
});
|
|
2822
3142
|
} catch (_error) {}
|
|
2823
3143
|
const summaryLines = [];
|
|
@@ -2838,7 +3158,8 @@ async function runInit(config, options = {}) {
|
|
|
2838
3158
|
try {
|
|
2839
3159
|
execSync(`pnpm update --latest ${bensandeeDeps.join(" ")}`, {
|
|
2840
3160
|
cwd: config.targetDir,
|
|
2841
|
-
stdio: "ignore"
|
|
3161
|
+
stdio: "ignore",
|
|
3162
|
+
timeout: 6e4
|
|
2842
3163
|
});
|
|
2843
3164
|
s.stop("Updated @bensandee/* packages");
|
|
2844
3165
|
} catch (_error) {
|
|
@@ -2847,10 +3168,8 @@ async function runInit(config, options = {}) {
|
|
|
2847
3168
|
}
|
|
2848
3169
|
p.note([
|
|
2849
3170
|
"1. Run: pnpm install",
|
|
2850
|
-
"2. Run: pnpm
|
|
2851
|
-
"3.
|
|
2852
|
-
"4. Run: pnpm test",
|
|
2853
|
-
...options.noPrompt ? [] : ["5. In Claude Code, run: \"Execute the steps in .tooling-migrate.md\""]
|
|
3171
|
+
"2. Run: pnpm check",
|
|
3172
|
+
...options.noPrompt ? [] : ["3. In Claude Code, run: \"Execute the steps in .tooling-migrate.md\""]
|
|
2854
3173
|
].join("\n"), "Next steps");
|
|
2855
3174
|
return results;
|
|
2856
3175
|
}
|
|
@@ -3180,7 +3499,7 @@ async function createRelease(executor, conn, tag) {
|
|
|
3180
3499
|
//#endregion
|
|
3181
3500
|
//#region src/release/log.ts
|
|
3182
3501
|
/** Log a debug message when verbose mode is enabled. */
|
|
3183
|
-
function debug
|
|
3502
|
+
function debug(config, message) {
|
|
3184
3503
|
if (config.verbose) p.log.info(`[debug] ${message}`);
|
|
3185
3504
|
}
|
|
3186
3505
|
/** Log the result of an exec call when verbose mode is enabled. */
|
|
@@ -3263,7 +3582,7 @@ function buildPrContent(executor, cwd, packagesBefore) {
|
|
|
3263
3582
|
async function runVersionMode(executor, config) {
|
|
3264
3583
|
p.log.info("Changesets detected — versioning packages");
|
|
3265
3584
|
const packagesBefore = executor.listWorkspacePackages(config.cwd);
|
|
3266
|
-
debug
|
|
3585
|
+
debug(config, `Packages before versioning: ${packagesBefore.map((pkg) => `${pkg.name}@${pkg.version}`).join(", ") || "(none)"}`);
|
|
3267
3586
|
const changesetConfigPath = path.join(config.cwd, ".changeset", "config.json");
|
|
3268
3587
|
const originalConfig = executor.readFile(changesetConfigPath);
|
|
3269
3588
|
if (originalConfig) {
|
|
@@ -3274,7 +3593,7 @@ async function runVersionMode(executor, config) {
|
|
|
3274
3593
|
commit: false
|
|
3275
3594
|
};
|
|
3276
3595
|
executor.writeFile(changesetConfigPath, JSON.stringify(patched, null, 2) + "\n");
|
|
3277
|
-
debug
|
|
3596
|
+
debug(config, "Temporarily disabled changeset commit:true");
|
|
3278
3597
|
}
|
|
3279
3598
|
}
|
|
3280
3599
|
const versionResult = executor.exec("pnpm changeset version", { cwd: config.cwd });
|
|
@@ -3283,11 +3602,12 @@ async function runVersionMode(executor, config) {
|
|
|
3283
3602
|
if (versionResult.exitCode !== 0) throw new FatalError(`pnpm changeset version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr}`);
|
|
3284
3603
|
debugExec(config, "pnpm install --no-frozen-lockfile", executor.exec("pnpm install --no-frozen-lockfile", { cwd: config.cwd }));
|
|
3285
3604
|
const { title, body } = buildPrContent(executor, config.cwd, packagesBefore);
|
|
3286
|
-
debug
|
|
3287
|
-
executor.exec("git add -A", { cwd: config.cwd });
|
|
3605
|
+
debug(config, `PR title: ${title}`);
|
|
3606
|
+
const addResult = executor.exec("git add -A", { cwd: config.cwd });
|
|
3607
|
+
if (addResult.exitCode !== 0) throw new FatalError(`git add failed: ${addResult.stderr || addResult.stdout}`);
|
|
3288
3608
|
const remainingChangesets = executor.listChangesetFiles(config.cwd);
|
|
3289
3609
|
if (remainingChangesets.length > 0) p.log.warn(`Changeset files still present after versioning: ${remainingChangesets.join(", ")}`);
|
|
3290
|
-
debug
|
|
3610
|
+
debug(config, `Changeset files after versioning: ${remainingChangesets.length > 0 ? remainingChangesets.join(", ") : "(none — all consumed)"}`);
|
|
3291
3611
|
const commitResult = executor.exec("git commit -m \"chore: version packages\"", { cwd: config.cwd });
|
|
3292
3612
|
debugExec(config, "git commit", commitResult);
|
|
3293
3613
|
if (commitResult.exitCode !== 0) {
|
|
@@ -3304,14 +3624,16 @@ async function runVersionMode(executor, config) {
|
|
|
3304
3624
|
pr: "none"
|
|
3305
3625
|
};
|
|
3306
3626
|
}
|
|
3307
|
-
|
|
3627
|
+
const pushResult = executor.exec(`git push origin "HEAD:refs/heads/${BRANCH}" --force`, { cwd: config.cwd });
|
|
3628
|
+
debugExec(config, "git push", pushResult);
|
|
3629
|
+
if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
|
|
3308
3630
|
const conn = {
|
|
3309
3631
|
serverUrl: config.serverUrl,
|
|
3310
3632
|
repository: config.repository,
|
|
3311
3633
|
token: config.token
|
|
3312
3634
|
};
|
|
3313
3635
|
const existingPr = await findOpenPr(executor, conn, BRANCH);
|
|
3314
|
-
debug
|
|
3636
|
+
debug(config, `Existing open PR for ${BRANCH}: ${existingPr === null ? "(none)" : `#${String(existingPr)}`}`);
|
|
3315
3637
|
if (existingPr === null) {
|
|
3316
3638
|
await createPr(executor, conn, {
|
|
3317
3639
|
title,
|
|
@@ -3359,14 +3681,14 @@ async function runPublishMode(executor, config) {
|
|
|
3359
3681
|
debugExec(config, "pnpm changeset publish", publishResult);
|
|
3360
3682
|
if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
|
|
3361
3683
|
const stdoutTags = parseNewTags(publishResult.stdout + "\n" + publishResult.stderr);
|
|
3362
|
-
debug
|
|
3684
|
+
debug(config, `Tags from publish stdout: ${stdoutTags.length > 0 ? stdoutTags.join(", ") : "(none)"}`);
|
|
3363
3685
|
const expectedTags = computeExpectedTags(executor.listWorkspacePackages(config.cwd));
|
|
3364
|
-
debug
|
|
3686
|
+
debug(config, `Expected tags from workspace packages: ${expectedTags.length > 0 ? expectedTags.join(", ") : "(none)"}`);
|
|
3365
3687
|
const remoteTags = parseRemoteTags(executor.exec("git ls-remote --tags origin", { cwd: config.cwd }).stdout);
|
|
3366
|
-
debug
|
|
3688
|
+
debug(config, `Remote tags: ${remoteTags.length > 0 ? remoteTags.join(", ") : "(none)"}`);
|
|
3367
3689
|
const remoteSet = new Set(remoteTags);
|
|
3368
3690
|
const tagsToPush = reconcileTags(expectedTags, remoteTags, stdoutTags);
|
|
3369
|
-
debug
|
|
3691
|
+
debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
|
|
3370
3692
|
if (config.dryRun) {
|
|
3371
3693
|
if (tagsToPush.length === 0) {
|
|
3372
3694
|
p.log.info("No packages were published");
|
|
@@ -3396,8 +3718,12 @@ async function runPublishMode(executor, config) {
|
|
|
3396
3718
|
const errors = [];
|
|
3397
3719
|
for (const tag of allTags) try {
|
|
3398
3720
|
if (!remoteSet.has(tag)) {
|
|
3399
|
-
if (executor.exec(`git tag -l ${JSON.stringify(tag)}`, { cwd: config.cwd }).stdout.trim() === "")
|
|
3400
|
-
|
|
3721
|
+
if (executor.exec(`git tag -l ${JSON.stringify(tag)}`, { cwd: config.cwd }).stdout.trim() === "") {
|
|
3722
|
+
const tagResult = executor.exec(`git tag ${JSON.stringify(tag)}`, { cwd: config.cwd });
|
|
3723
|
+
if (tagResult.exitCode !== 0) throw new FatalError(`Failed to create tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
3724
|
+
}
|
|
3725
|
+
const pushTagResult = executor.exec(`git push origin refs/tags/${tag}`, { cwd: config.cwd });
|
|
3726
|
+
if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
|
|
3401
3727
|
}
|
|
3402
3728
|
if (await findRelease(executor, conn, tag)) p.log.warn(`Release for ${tag} already exists — skipping`);
|
|
3403
3729
|
else {
|
|
@@ -3542,18 +3868,18 @@ function getCurrentBranch(executor, cwd) {
|
|
|
3542
3868
|
async function runRelease(config, executor) {
|
|
3543
3869
|
const branch = getCurrentBranch(executor, config.cwd);
|
|
3544
3870
|
if (branch !== "main") {
|
|
3545
|
-
debug
|
|
3871
|
+
debug(config, `Skipping release on non-main branch: ${branch}`);
|
|
3546
3872
|
return { mode: "none" };
|
|
3547
3873
|
}
|
|
3548
3874
|
executor.exec("git config user.name \"forgejo-actions[bot]\"", { cwd: config.cwd });
|
|
3549
3875
|
executor.exec("git config user.email \"forgejo-actions[bot]@noreply.localhost\"", { cwd: config.cwd });
|
|
3550
3876
|
const changesetFiles = executor.listChangesetFiles(config.cwd);
|
|
3551
|
-
debug
|
|
3877
|
+
debug(config, `Changeset files found: ${changesetFiles.length > 0 ? changesetFiles.join(", ") : "(none)"}`);
|
|
3552
3878
|
if (changesetFiles.length > 0) {
|
|
3553
|
-
debug
|
|
3879
|
+
debug(config, "Entering version mode");
|
|
3554
3880
|
return runVersionMode(executor, config);
|
|
3555
3881
|
}
|
|
3556
|
-
debug
|
|
3882
|
+
debug(config, "Entering publish mode");
|
|
3557
3883
|
return runPublishMode(executor, config);
|
|
3558
3884
|
}
|
|
3559
3885
|
//#endregion
|
|
@@ -3589,7 +3915,8 @@ async function triggerForgejo(conn, ref) {
|
|
|
3589
3915
|
p.log.info(`Triggered release workflow on Forgejo (ref: ${ref})`);
|
|
3590
3916
|
}
|
|
3591
3917
|
function triggerGitHub(ref) {
|
|
3592
|
-
createRealExecutor().exec(`gh workflow run release.yml --ref ${ref}`, { cwd: process.cwd() });
|
|
3918
|
+
const result = createRealExecutor().exec(`gh workflow run release.yml --ref ${ref}`, { cwd: process.cwd() });
|
|
3919
|
+
if (result.exitCode !== 0) throw new FatalError(`Failed to trigger GitHub workflow: ${result.stderr || result.stdout || "unknown error"}`);
|
|
3593
3920
|
p.log.info(`Triggered release workflow on GitHub (ref: ${ref})`);
|
|
3594
3921
|
}
|
|
3595
3922
|
//#endregion
|
|
@@ -3658,7 +3985,8 @@ function mergeGitHub(dryRun) {
|
|
|
3658
3985
|
p.log.info(`[dry-run] Would merge PR #${prNum} and delete branch ${HEAD_BRANCH}`);
|
|
3659
3986
|
return;
|
|
3660
3987
|
}
|
|
3661
|
-
executor.exec(`gh pr merge ${HEAD_BRANCH} --merge --delete-branch`, { cwd: process.cwd() });
|
|
3988
|
+
const result = executor.exec(`gh pr merge ${HEAD_BRANCH} --merge --delete-branch`, { cwd: process.cwd() });
|
|
3989
|
+
if (result.exitCode !== 0) throw new FatalError(`Failed to merge PR: ${result.stderr || result.stdout || "unknown error"}`);
|
|
3662
3990
|
p.log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
|
|
3663
3991
|
}
|
|
3664
3992
|
//#endregion
|
|
@@ -3697,7 +4025,7 @@ async function runSimpleRelease(executor, config) {
|
|
|
3697
4025
|
debugExec(config, "commit-and-tag-version", versionResult);
|
|
3698
4026
|
if (versionResult.exitCode !== 0) throw new FatalError(`commit-and-tag-version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr || versionResult.stdout}`);
|
|
3699
4027
|
const version = readVersion(executor, config.cwd);
|
|
3700
|
-
debug
|
|
4028
|
+
debug(config, `New version: ${version}`);
|
|
3701
4029
|
const tagResult = executor.exec("git describe --tags --abbrev=0", { cwd: config.cwd });
|
|
3702
4030
|
debugExec(config, "git describe", tagResult);
|
|
3703
4031
|
const tag = tagResult.stdout.trim();
|
|
@@ -3719,7 +4047,7 @@ async function runSimpleRelease(executor, config) {
|
|
|
3719
4047
|
let pushed = false;
|
|
3720
4048
|
if (!config.noPush) {
|
|
3721
4049
|
const branch = executor.exec("git rev-parse --abbrev-ref HEAD", { cwd: config.cwd }).stdout.trim() || "main";
|
|
3722
|
-
debug
|
|
4050
|
+
debug(config, `Pushing to origin/${branch}`);
|
|
3723
4051
|
const pushResult = executor.exec(`git push --follow-tags origin ${branch}`, { cwd: config.cwd });
|
|
3724
4052
|
debugExec(config, "git push", pushResult);
|
|
3725
4053
|
if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
|
|
@@ -3729,7 +4057,10 @@ async function runSimpleRelease(executor, config) {
|
|
|
3729
4057
|
let slidingTags = [];
|
|
3730
4058
|
if (!config.noSlidingTags && pushed) {
|
|
3731
4059
|
slidingTags = computeSlidingTags(version);
|
|
3732
|
-
for (const slidingTag of slidingTags)
|
|
4060
|
+
for (const slidingTag of slidingTags) {
|
|
4061
|
+
const slidingTagResult = executor.exec(`git tag -f ${slidingTag}`, { cwd: config.cwd });
|
|
4062
|
+
if (slidingTagResult.exitCode !== 0) throw new FatalError(`Failed to create sliding tag ${slidingTag}: ${slidingTagResult.stderr || slidingTagResult.stdout}`);
|
|
4063
|
+
}
|
|
3733
4064
|
const forcePushResult = executor.exec(`git push origin ${slidingTags.join(" ")} --force`, { cwd: config.cwd });
|
|
3734
4065
|
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
3735
4066
|
if (forcePushResult.exitCode !== 0) p.log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
@@ -3749,7 +4080,7 @@ async function createPlatformRelease(executor, config, tag) {
|
|
|
3749
4080
|
if (!config.platform) return false;
|
|
3750
4081
|
if (config.platform.type === "forgejo") {
|
|
3751
4082
|
if (await findRelease(executor, config.platform.conn, tag)) {
|
|
3752
|
-
debug
|
|
4083
|
+
debug(config, `Release for ${tag} already exists, skipping`);
|
|
3753
4084
|
return false;
|
|
3754
4085
|
}
|
|
3755
4086
|
await createRelease(executor, config.platform.conn, tag);
|
|
@@ -3948,290 +4279,6 @@ const runChecksCommand = defineCommand({
|
|
|
3948
4279
|
}
|
|
3949
4280
|
});
|
|
3950
4281
|
//#endregion
|
|
3951
|
-
//#region src/release/docker.ts
|
|
3952
|
-
const ToolingDockerMapSchema = z.record(z.string(), z.object({
|
|
3953
|
-
dockerfile: z.string(),
|
|
3954
|
-
context: z.string().default(".")
|
|
3955
|
-
}));
|
|
3956
|
-
const ToolingConfigDockerSchema = z.object({ docker: ToolingDockerMapSchema.optional() });
|
|
3957
|
-
const PackageInfoSchema = z.object({
|
|
3958
|
-
name: z.string().optional(),
|
|
3959
|
-
version: z.string().optional()
|
|
3960
|
-
});
|
|
3961
|
-
/** Read the docker map from .tooling.json. Returns empty record if missing or invalid. */
|
|
3962
|
-
function loadDockerMap(executor, cwd) {
|
|
3963
|
-
const configPath = path.join(cwd, ".tooling.json");
|
|
3964
|
-
const raw = executor.readFile(configPath);
|
|
3965
|
-
if (!raw) return {};
|
|
3966
|
-
try {
|
|
3967
|
-
const result = ToolingConfigDockerSchema.safeParse(JSON.parse(raw));
|
|
3968
|
-
if (!result.success || !result.data.docker) return {};
|
|
3969
|
-
return result.data.docker;
|
|
3970
|
-
} catch (_error) {
|
|
3971
|
-
return {};
|
|
3972
|
-
}
|
|
3973
|
-
}
|
|
3974
|
-
/** Read name and version from a package's package.json. */
|
|
3975
|
-
function readPackageInfo(executor, packageJsonPath) {
|
|
3976
|
-
const raw = executor.readFile(packageJsonPath);
|
|
3977
|
-
if (!raw) return {
|
|
3978
|
-
name: void 0,
|
|
3979
|
-
version: void 0
|
|
3980
|
-
};
|
|
3981
|
-
try {
|
|
3982
|
-
const result = PackageInfoSchema.safeParse(JSON.parse(raw));
|
|
3983
|
-
if (!result.success) return {
|
|
3984
|
-
name: void 0,
|
|
3985
|
-
version: void 0
|
|
3986
|
-
};
|
|
3987
|
-
return {
|
|
3988
|
-
name: result.data.name,
|
|
3989
|
-
version: result.data.version
|
|
3990
|
-
};
|
|
3991
|
-
} catch (_error) {
|
|
3992
|
-
return {
|
|
3993
|
-
name: void 0,
|
|
3994
|
-
version: void 0
|
|
3995
|
-
};
|
|
3996
|
-
}
|
|
3997
|
-
}
|
|
3998
|
-
/** Convention paths to check for Dockerfiles in a package directory. */
|
|
3999
|
-
const CONVENTION_DOCKERFILE_PATHS = ["Dockerfile", "docker/Dockerfile"];
|
|
4000
|
-
/**
|
|
4001
|
-
* Find a Dockerfile at convention paths for a monorepo package.
|
|
4002
|
-
* Checks packages/{dir}/Dockerfile and packages/{dir}/docker/Dockerfile.
|
|
4003
|
-
*/
|
|
4004
|
-
function findConventionDockerfile(executor, cwd, dir) {
|
|
4005
|
-
for (const rel of CONVENTION_DOCKERFILE_PATHS) {
|
|
4006
|
-
const dockerfilePath = `packages/${dir}/${rel}`;
|
|
4007
|
-
if (executor.readFile(path.join(cwd, dockerfilePath)) !== null) return {
|
|
4008
|
-
dockerfile: dockerfilePath,
|
|
4009
|
-
context: "."
|
|
4010
|
-
};
|
|
4011
|
-
}
|
|
4012
|
-
}
|
|
4013
|
-
/**
|
|
4014
|
-
* Find a Dockerfile at convention paths for a single-package repo.
|
|
4015
|
-
* Checks Dockerfile and docker/Dockerfile at the project root.
|
|
4016
|
-
*/
|
|
4017
|
-
function findRootDockerfile(executor, cwd) {
|
|
4018
|
-
for (const rel of CONVENTION_DOCKERFILE_PATHS) if (executor.readFile(path.join(cwd, rel)) !== null) return {
|
|
4019
|
-
dockerfile: rel,
|
|
4020
|
-
context: "."
|
|
4021
|
-
};
|
|
4022
|
-
}
|
|
4023
|
-
/**
|
|
4024
|
-
* Discover Docker packages by convention and merge with .tooling.json overrides.
|
|
4025
|
-
*
|
|
4026
|
-
* Convention: any package with a Dockerfile or docker/Dockerfile is a Docker package.
|
|
4027
|
-
* For monorepos, scans packages/{name}/. For single-package repos, scans the root.
|
|
4028
|
-
* The docker map in .tooling.json overrides convention-discovered config and can add
|
|
4029
|
-
* packages at non-standard locations.
|
|
4030
|
-
*
|
|
4031
|
-
* Image names are derived from {root-name}-{package-name} using each package's package.json name.
|
|
4032
|
-
* Versions are read from each package's own package.json.
|
|
4033
|
-
*/
|
|
4034
|
-
function detectDockerPackages(executor, cwd, repoName) {
|
|
4035
|
-
const overrides = loadDockerMap(executor, cwd);
|
|
4036
|
-
const packageDirs = executor.listPackageDirs(cwd);
|
|
4037
|
-
const packages = [];
|
|
4038
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4039
|
-
if (packageDirs.length > 0) {
|
|
4040
|
-
for (const dir of packageDirs) {
|
|
4041
|
-
const convention = findConventionDockerfile(executor, cwd, dir);
|
|
4042
|
-
const docker = overrides[dir] ?? convention;
|
|
4043
|
-
if (docker) {
|
|
4044
|
-
const { name, version } = readPackageInfo(executor, path.join(cwd, "packages", dir, "package.json"));
|
|
4045
|
-
packages.push({
|
|
4046
|
-
dir,
|
|
4047
|
-
imageName: `${repoName}-${name ?? dir}`,
|
|
4048
|
-
version,
|
|
4049
|
-
docker
|
|
4050
|
-
});
|
|
4051
|
-
seen.add(dir);
|
|
4052
|
-
}
|
|
4053
|
-
}
|
|
4054
|
-
for (const [dir, docker] of Object.entries(overrides)) if (!seen.has(dir)) {
|
|
4055
|
-
const { name, version } = readPackageInfo(executor, path.join(cwd, "packages", dir, "package.json"));
|
|
4056
|
-
packages.push({
|
|
4057
|
-
dir,
|
|
4058
|
-
imageName: `${repoName}-${name ?? dir}`,
|
|
4059
|
-
version,
|
|
4060
|
-
docker
|
|
4061
|
-
});
|
|
4062
|
-
}
|
|
4063
|
-
} else {
|
|
4064
|
-
const convention = findRootDockerfile(executor, cwd);
|
|
4065
|
-
const docker = overrides["."] ?? convention;
|
|
4066
|
-
if (docker) {
|
|
4067
|
-
const { name, version } = readPackageInfo(executor, path.join(cwd, "package.json"));
|
|
4068
|
-
packages.push({
|
|
4069
|
-
dir: ".",
|
|
4070
|
-
imageName: name ?? repoName,
|
|
4071
|
-
version,
|
|
4072
|
-
docker
|
|
4073
|
-
});
|
|
4074
|
-
}
|
|
4075
|
-
}
|
|
4076
|
-
return packages;
|
|
4077
|
-
}
|
|
4078
|
-
/**
|
|
4079
|
-
* Read docker config for a single package, checking convention paths first,
|
|
4080
|
-
* then .tooling.json overrides. Used by the per-package image:build script.
|
|
4081
|
-
*/
|
|
4082
|
-
function readSinglePackageDocker(executor, cwd, packageDir, repoName) {
|
|
4083
|
-
const dir = path.basename(path.resolve(cwd, packageDir));
|
|
4084
|
-
const convention = findConventionDockerfile(executor, cwd, dir);
|
|
4085
|
-
const docker = loadDockerMap(executor, cwd)[dir] ?? convention;
|
|
4086
|
-
if (!docker) throw new FatalError(`No Dockerfile found for package "${dir}" (checked convention paths and .tooling.json)`);
|
|
4087
|
-
const { name, version } = readPackageInfo(executor, path.join(cwd, "packages", dir, "package.json"));
|
|
4088
|
-
return {
|
|
4089
|
-
dir,
|
|
4090
|
-
imageName: `${repoName}-${name ?? dir}`,
|
|
4091
|
-
version,
|
|
4092
|
-
docker
|
|
4093
|
-
};
|
|
4094
|
-
}
|
|
4095
|
-
/** Parse semver version string into major, minor, patch components. */
|
|
4096
|
-
function parseSemver(version) {
|
|
4097
|
-
const clean = version.replace(/^v/, "");
|
|
4098
|
-
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(clean);
|
|
4099
|
-
if (!match?.[1] || !match[2] || !match[3]) throw new FatalError(`Invalid semver version: ${version}`);
|
|
4100
|
-
return {
|
|
4101
|
-
major: Number(match[1]),
|
|
4102
|
-
minor: Number(match[2]),
|
|
4103
|
-
patch: Number(match[3])
|
|
4104
|
-
};
|
|
4105
|
-
}
|
|
4106
|
-
/** Generate semver tag variants: latest, vX.Y.Z, vX.Y, vX */
|
|
4107
|
-
function generateTags(version) {
|
|
4108
|
-
const { major, minor, patch } = parseSemver(version);
|
|
4109
|
-
return [
|
|
4110
|
-
"latest",
|
|
4111
|
-
`v${major}.${minor}.${patch}`,
|
|
4112
|
-
`v${major}.${minor}`,
|
|
4113
|
-
`v${major}`
|
|
4114
|
-
];
|
|
4115
|
-
}
|
|
4116
|
-
/** Build the full image reference: namespace/imageName:tag */
|
|
4117
|
-
function imageRef(namespace, imageName, tag) {
|
|
4118
|
-
return `${namespace}/${imageName}:${tag}`;
|
|
4119
|
-
}
|
|
4120
|
-
function log$1(message) {
|
|
4121
|
-
console.log(message);
|
|
4122
|
-
}
|
|
4123
|
-
function debug(verbose, message) {
|
|
4124
|
-
if (verbose) console.log(`[debug] ${message}`);
|
|
4125
|
-
}
|
|
4126
|
-
/** Read the repo name from root package.json. */
|
|
4127
|
-
function readRepoName(executor, cwd) {
|
|
4128
|
-
const rootPkgRaw = executor.readFile(path.join(cwd, "package.json"));
|
|
4129
|
-
if (!rootPkgRaw) throw new FatalError("No package.json found in project root");
|
|
4130
|
-
const repoName = parsePackageJson(rootPkgRaw)?.name;
|
|
4131
|
-
if (!repoName) throw new FatalError("Root package.json must have a name field");
|
|
4132
|
-
return repoName;
|
|
4133
|
-
}
|
|
4134
|
-
/** Build a single docker image from its config. Paths are resolved relative to cwd. */
|
|
4135
|
-
function buildImage(executor, pkg, cwd, verbose, extraArgs) {
|
|
4136
|
-
const dockerfilePath = path.resolve(cwd, pkg.docker.dockerfile);
|
|
4137
|
-
const contextPath = path.resolve(cwd, pkg.docker.context);
|
|
4138
|
-
const command = [
|
|
4139
|
-
"docker build",
|
|
4140
|
-
`-f ${dockerfilePath}`,
|
|
4141
|
-
`-t ${pkg.imageName}:latest`,
|
|
4142
|
-
...extraArgs,
|
|
4143
|
-
contextPath
|
|
4144
|
-
].join(" ");
|
|
4145
|
-
debug(verbose, `Running: ${command}`);
|
|
4146
|
-
const buildResult = executor.exec(command);
|
|
4147
|
-
debug(verbose, `Build stdout: ${buildResult.stdout}`);
|
|
4148
|
-
if (buildResult.exitCode !== 0) throw new FatalError(`docker build failed for ${pkg.dir} (exit ${buildResult.exitCode}): ${buildResult.stderr}`);
|
|
4149
|
-
}
|
|
4150
|
-
/**
|
|
4151
|
-
* Detect packages with docker config in .tooling.json and build each one.
|
|
4152
|
-
* Runs `docker build -f <dockerfile> -t <image-name>:latest <context>` for each package.
|
|
4153
|
-
* Dockerfile and context paths are resolved relative to the project root.
|
|
4154
|
-
*
|
|
4155
|
-
* When `packageDir` is set, builds only that single package (for use as an image:build script).
|
|
4156
|
-
*/
|
|
4157
|
-
function runDockerBuild(executor, config) {
|
|
4158
|
-
const repoName = readRepoName(executor, config.cwd);
|
|
4159
|
-
if (config.packageDir) {
|
|
4160
|
-
const pkg = readSinglePackageDocker(executor, config.cwd, config.packageDir, repoName);
|
|
4161
|
-
log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
4162
|
-
buildImage(executor, pkg, config.cwd, config.verbose, config.extraArgs);
|
|
4163
|
-
log$1(`Built ${pkg.imageName}:latest`);
|
|
4164
|
-
return { packages: [pkg] };
|
|
4165
|
-
}
|
|
4166
|
-
const packages = detectDockerPackages(executor, config.cwd, repoName);
|
|
4167
|
-
if (packages.length === 0) {
|
|
4168
|
-
log$1("No packages with docker config found");
|
|
4169
|
-
return { packages: [] };
|
|
4170
|
-
}
|
|
4171
|
-
log$1(`Found ${packages.length} Docker package(s): ${packages.map((p) => p.dir).join(", ")}`);
|
|
4172
|
-
for (const pkg of packages) {
|
|
4173
|
-
log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
4174
|
-
buildImage(executor, pkg, config.cwd, config.verbose, config.extraArgs);
|
|
4175
|
-
}
|
|
4176
|
-
log$1(`Built ${packages.length} image(s)`);
|
|
4177
|
-
return { packages };
|
|
4178
|
-
}
|
|
4179
|
-
/**
|
|
4180
|
-
* Run the full Docker publish pipeline:
|
|
4181
|
-
* 1. Build all images via runDockerBuild
|
|
4182
|
-
* 2. Login to registry
|
|
4183
|
-
* 3. Tag each image with semver variants from its own package.json version
|
|
4184
|
-
* 4. Push all tags
|
|
4185
|
-
* 5. Logout from registry
|
|
4186
|
-
*/
|
|
4187
|
-
function runDockerPublish(executor, config) {
|
|
4188
|
-
const { packages } = runDockerBuild(executor, {
|
|
4189
|
-
cwd: config.cwd,
|
|
4190
|
-
packageDir: void 0,
|
|
4191
|
-
verbose: config.verbose,
|
|
4192
|
-
extraArgs: []
|
|
4193
|
-
});
|
|
4194
|
-
if (packages.length === 0) return {
|
|
4195
|
-
packages: [],
|
|
4196
|
-
tags: []
|
|
4197
|
-
};
|
|
4198
|
-
for (const pkg of packages) if (!pkg.version) throw new FatalError(`Package ${pkg.dir} has docker config but no version in package.json`);
|
|
4199
|
-
if (!config.dryRun) {
|
|
4200
|
-
log$1(`Logging in to ${config.registryHost}...`);
|
|
4201
|
-
const loginResult = executor.exec(`echo "${config.password}" | docker login ${config.registryHost} -u ${config.username} --password-stdin`);
|
|
4202
|
-
if (loginResult.exitCode !== 0) throw new FatalError(`Docker login failed: ${loginResult.stderr}`);
|
|
4203
|
-
} else log$1("[dry-run] Skipping docker login");
|
|
4204
|
-
const allTags = [];
|
|
4205
|
-
try {
|
|
4206
|
-
for (const pkg of packages) {
|
|
4207
|
-
const tags = generateTags(pkg.version ?? "");
|
|
4208
|
-
log$1(`${pkg.dir} v${pkg.version} → tags: ${tags.join(", ")}`);
|
|
4209
|
-
for (const tag of tags) {
|
|
4210
|
-
const ref = imageRef(config.registryNamespace, pkg.imageName, tag);
|
|
4211
|
-
allTags.push(ref);
|
|
4212
|
-
log$1(`Tagging ${pkg.imageName} → ${ref}`);
|
|
4213
|
-
const tagResult = executor.exec(`docker tag ${pkg.imageName} ${ref}`);
|
|
4214
|
-
if (tagResult.exitCode !== 0) throw new FatalError(`docker tag failed: ${tagResult.stderr}`);
|
|
4215
|
-
if (!config.dryRun) {
|
|
4216
|
-
log$1(`Pushing ${ref}...`);
|
|
4217
|
-
const pushResult = executor.exec(`docker push ${ref}`);
|
|
4218
|
-
if (pushResult.exitCode !== 0) throw new FatalError(`docker push failed: ${pushResult.stderr}`);
|
|
4219
|
-
} else log$1(`[dry-run] Skipping push for ${ref}`);
|
|
4220
|
-
}
|
|
4221
|
-
}
|
|
4222
|
-
} finally {
|
|
4223
|
-
if (!config.dryRun) {
|
|
4224
|
-
log$1(`Logging out from ${config.registryHost}...`);
|
|
4225
|
-
executor.exec(`docker logout ${config.registryHost}`);
|
|
4226
|
-
}
|
|
4227
|
-
}
|
|
4228
|
-
log$1(`Published ${allTags.length} image tag(s)`);
|
|
4229
|
-
return {
|
|
4230
|
-
packages,
|
|
4231
|
-
tags: allTags
|
|
4232
|
-
};
|
|
4233
|
-
}
|
|
4234
|
-
//#endregion
|
|
4235
4282
|
//#region src/commands/publish-docker.ts
|
|
4236
4283
|
function requireEnv(name) {
|
|
4237
4284
|
const value = process.env[name];
|
|
@@ -4268,6 +4315,22 @@ const publishDockerCommand = defineCommand({
|
|
|
4268
4315
|
});
|
|
4269
4316
|
//#endregion
|
|
4270
4317
|
//#region src/commands/docker-build.ts
|
|
4318
|
+
/**
|
|
4319
|
+
* Detect if cwd is inside a packages/ subdirectory.
|
|
4320
|
+
* If so, return the project root and the relative package dir.
|
|
4321
|
+
* Otherwise return cwd as-is with no packageDir.
|
|
4322
|
+
*/
|
|
4323
|
+
function detectSubpackage(cwd) {
|
|
4324
|
+
const parent = path.dirname(cwd);
|
|
4325
|
+
if (path.basename(parent) === "packages" && existsSync(path.join(parent, "..", "package.json"))) return {
|
|
4326
|
+
root: path.dirname(parent),
|
|
4327
|
+
packageDir: `packages/${path.basename(cwd)}`
|
|
4328
|
+
};
|
|
4329
|
+
return {
|
|
4330
|
+
root: cwd,
|
|
4331
|
+
packageDir: void 0
|
|
4332
|
+
};
|
|
4333
|
+
}
|
|
4271
4334
|
const dockerBuildCommand = defineCommand({
|
|
4272
4335
|
meta: {
|
|
4273
4336
|
name: "docker:build",
|
|
@@ -4292,9 +4355,16 @@ const dockerBuildCommand = defineCommand({
|
|
|
4292
4355
|
const executor = createRealExecutor();
|
|
4293
4356
|
const rawExtra = args._ ?? [];
|
|
4294
4357
|
const extraArgs = Array.isArray(rawExtra) ? rawExtra.map(String) : [String(rawExtra)];
|
|
4358
|
+
let cwd = process.cwd();
|
|
4359
|
+
let packageDir = args.package;
|
|
4360
|
+
if (!packageDir) {
|
|
4361
|
+
const detected = detectSubpackage(cwd);
|
|
4362
|
+
cwd = detected.root;
|
|
4363
|
+
packageDir = detected.packageDir;
|
|
4364
|
+
}
|
|
4295
4365
|
runDockerBuild(executor, {
|
|
4296
|
-
cwd
|
|
4297
|
-
packageDir
|
|
4366
|
+
cwd,
|
|
4367
|
+
packageDir,
|
|
4298
4368
|
verbose: args.verbose === true,
|
|
4299
4369
|
extraArgs: extraArgs.filter((a) => a.length > 0)
|
|
4300
4370
|
});
|
|
@@ -4310,16 +4380,16 @@ const COMPOSE_FILE_CANDIDATES = [
|
|
|
4310
4380
|
"compose.yml"
|
|
4311
4381
|
];
|
|
4312
4382
|
/** Zod schema for the subset of compose YAML we care about. */
|
|
4313
|
-
const ComposePortSchema = z.union([z.string(), z.
|
|
4383
|
+
const ComposePortSchema = z.union([z.string(), z.looseObject({
|
|
4314
4384
|
published: z.union([z.string(), z.number()]),
|
|
4315
4385
|
target: z.union([z.string(), z.number()]).optional()
|
|
4316
|
-
})
|
|
4317
|
-
const ComposeServiceSchema = z.
|
|
4386
|
+
})]);
|
|
4387
|
+
const ComposeServiceSchema = z.looseObject({
|
|
4318
4388
|
image: z.string().optional(),
|
|
4319
4389
|
ports: z.array(ComposePortSchema).optional(),
|
|
4320
4390
|
healthcheck: z.unknown().optional()
|
|
4321
|
-
})
|
|
4322
|
-
const ComposeFileSchema = z.
|
|
4391
|
+
});
|
|
4392
|
+
const ComposeFileSchema = z.looseObject({ services: z.record(z.string(), ComposeServiceSchema).optional() });
|
|
4323
4393
|
/** Directories to scan for compose files, in priority order. */
|
|
4324
4394
|
const COMPOSE_DIR_CANDIDATES = [".", "docker"];
|
|
4325
4395
|
/** Detect which compose files exist at conventional paths.
|
|
@@ -4398,11 +4468,20 @@ function parseComposeServices(cwd, composeFiles) {
|
|
|
4398
4468
|
}
|
|
4399
4469
|
return [...serviceMap.values()];
|
|
4400
4470
|
}
|
|
4471
|
+
/**
|
|
4472
|
+
* Strip Docker Compose variable substitutions from an image string.
|
|
4473
|
+
* Handles `${VAR:-default}`, `${VAR-default}`, `${VAR:+alt}`, `${VAR+alt}`,
|
|
4474
|
+
* `${VAR:?err}`, `${VAR?err}`, and plain `${VAR}`.
|
|
4475
|
+
* Nested braces are not supported.
|
|
4476
|
+
*/
|
|
4477
|
+
function stripComposeVariables(image) {
|
|
4478
|
+
return image.replace(/\$\{[^}]*\}/g, "");
|
|
4479
|
+
}
|
|
4401
4480
|
/** Extract deduplicated bare image names (without tags) from parsed services. */
|
|
4402
4481
|
function extractComposeImageNames(services) {
|
|
4403
4482
|
const names = /* @__PURE__ */ new Set();
|
|
4404
4483
|
for (const service of services) if (service.image) {
|
|
4405
|
-
const bare = service.image.split(":")[0];
|
|
4484
|
+
const bare = stripComposeVariables(service.image).split(":")[0];
|
|
4406
4485
|
if (bare) names.add(bare);
|
|
4407
4486
|
}
|
|
4408
4487
|
return [...names];
|
|
@@ -4587,7 +4666,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
4587
4666
|
const main = defineCommand({
|
|
4588
4667
|
meta: {
|
|
4589
4668
|
name: "tooling",
|
|
4590
|
-
version: "0.
|
|
4669
|
+
version: "0.25.0",
|
|
4591
4670
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
4592
4671
|
},
|
|
4593
4672
|
subCommands: {
|
|
@@ -4603,7 +4682,11 @@ const main = defineCommand({
|
|
|
4603
4682
|
"docker:check": dockerCheckCommand
|
|
4604
4683
|
}
|
|
4605
4684
|
});
|
|
4606
|
-
console.log(`@bensandee/tooling v0.
|
|
4607
|
-
|
|
4685
|
+
console.log(`@bensandee/tooling v0.25.0`);
|
|
4686
|
+
async function run() {
|
|
4687
|
+
await runMain(main);
|
|
4688
|
+
process.exit(process.exitCode ?? 0);
|
|
4689
|
+
}
|
|
4690
|
+
run();
|
|
4608
4691
|
//#endregion
|
|
4609
4692
|
export {};
|
package/dist/index.d.mts
CHANGED
|
@@ -15,6 +15,9 @@ declare const PackageJsonSchema: z.ZodObject<{
|
|
|
15
15
|
types: z.ZodOptional<z.ZodString>;
|
|
16
16
|
typings: z.ZodOptional<z.ZodString>;
|
|
17
17
|
engines: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
18
|
+
repository: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
19
|
+
url: z.ZodString;
|
|
20
|
+
}, z.core.$loose>]>>;
|
|
18
21
|
}, z.core.$loose>;
|
|
19
22
|
type PackageJson = z.infer<typeof PackageJsonSchema>;
|
|
20
23
|
//#endregion
|
|
@@ -91,6 +94,8 @@ interface DetectedProjectState {
|
|
|
91
94
|
hasReleaseItConfig: boolean;
|
|
92
95
|
hasSimpleReleaseConfig: boolean;
|
|
93
96
|
hasChangesetsConfig: boolean;
|
|
97
|
+
/** Whether package.json has a repository field (needed for release workflows) */
|
|
98
|
+
hasRepositoryField: boolean;
|
|
94
99
|
/** Legacy tooling configs found */
|
|
95
100
|
legacyConfigs: LegacyConfig[];
|
|
96
101
|
}
|