UNPKG

@halospv3/hce.shared-config

Version:

Automate commit message quality, changelogs, and CI/CD releases. Its `main` entry point is a Semantic Release config. Functions and classes are exposed for customization. An ESLint config, a Commitlint config, and addl. resources for .NET projects are als

286 lines (285 loc) 14.5 kB
import { MSBuildProjectProperties } from "./MSBuildProjectProperties.mjs"; import { MSBuildProject } from "./MSBuildProject.mjs"; import { NugetRegistryInfo } from "./NugetRegistryInfo.mjs"; import { cwd } from "node:process"; import { type } from "arktype"; import * as path$1 from "node:path"; //#region src/dotnet/helpers.ts const ourDefaultPubDirectory = path$1.join(".", "publish"); /** * Build a prepareCmd string from .NET projects.\ * This will include a `dotnet publish` for each project's RID and TFM permutation,\ * `dotnet pack` for each project with output paths separated by NuGet Source and PackageId,\ * and `dotnet nuget sign` for each nupkg output directory. * @todo parse Solution files to publish all projects with default Publish parameters (as evaluated by MSBuild). * @param projectsToPublish An array of relative or full file paths of `.csproj` * projects -OR- an array of {@link MSBuildProject} objects. * The project paths will be passed to `dotnet publish` commands. * @param projectsToPackAndPush * Relative and/or full file paths of projects to pass to `dotnet pack`. If * string[], only the default NuGet Source will be used. If GitHub, GitLab, * etc. are also desired, pass {@link NugetRegistryInfo}[] * @param dotnetNugetSignOpts A {@link DotnetNugetSignOptions} object. The value * of the `--output` argument will be set to {@link ourDefaultPubDir} if `undefined`. * @returns A single string of CLI commands joined by ' && ' */ async function configurePrepareCmd(projectsToPublish, projectsToPackAndPush, dotnetNugetSignOpts) { const evaluatedProjects = projectsToPublish.filter((p) => p instanceof MSBuildProject); if (projectsToPackAndPush) { for (const project of projectsToPackAndPush) if (project instanceof NugetRegistryInfo) evaluatedProjects.push(project.project); } return [ await formatDotnetPublish(projectsToPublish), await formatDotnetPack(projectsToPackAndPush ?? []), formatDotnetNugetSign(dotnetNugetSignOpts) ].filter((v) => v !== void 0).join(" && "); /** * Create a string of CLI commands to run `dotnet publish` or the Publish * MSBuild target for one or more projects. * @async * @param projectsToPublish An array of one or more projects, either * pre-evaluated (see {@link MSBuildProject.Evaluate}) or as full file paths.\ * NOTE: Although `dotnet publish` allows directory or Solution file (.sln, * .slnx) paths, this function expects projects' full or relative file * paths. * @returns A Promise of a string. This string contains one or more `dotnet publish` * commands conjoined by " && ". It may also include one or more * `dotnet msbuild ${...} -restore -t:PublishAll -p:Configuration=Release` commands. */ async function formatDotnetPublish(projectsToPublish) { if (!Array.isArray(projectsToPublish) || projectsToPublish.length === 0) throw new Error(`Type of projectsToPublish (${typeof projectsToPublish}) is not allowed. Expected a string[] or MSBuildProject[] where length > 0.`); const evaluatedPublishProjects = await Promise.all(projectsToPublish.map(async (proj) => { if (proj instanceof MSBuildProject) return proj; const filteredProjects = evaluatedProjects.filter((p) => p.Properties.MSBuildProjectFullPath === MSBuildProjectProperties.GetFullPath(proj)); if (filteredProjects.length === 0) { const _proj = await MSBuildProject.Evaluate({ FullName: proj, GetProperty: MSBuildProject.MatrixProperties, GetItem: [], GetTargetResult: [], Property: {}, Targets: ["Restore"] }); evaluatedProjects.push(_proj); return _proj; } /** * Finds and returns the subjectively "best" project in {@link filteredProjects} * @returns the subjective "best" project in {@link filteredProjects} */ function getBest() { let best; if (filteredProjects.length > 0 && (best = filteredProjects[0]) instanceof MSBuildProject) return best; throw new Error("No MSBuildProjects could be found!"); } return getBest(); })); /** * Returns an array of one or more `dotnet` arguments. * @param proj An {@link MSBuildProject} to be published for one or more * runtime-framework combinations. * @returns If {@link proj} imports {@link ../../dotnet/PublishAll.targets}... * ``` * [`${proj.Properties.MSBuildProjectFullPath} -restore -t:PublishAll -p:Configuration=Release`] * ``` * Else, an array of `dotnet publish` arguments permutations e.g. * ``` * [ * 'myProj.csproj --runtime win7-x86 --framework net6.0', * 'myProj.csproj --runtime win7-x64 --framework net6.0' * ] * ``` * @example * const publishCmdArray = []; * const permutations = getPublishArgsPermutations(msbuildProject); * for (const permutation of permutations) { * if (permutation[0] === 'PublishAll') { * // 'dotnet msbuild full/path/to/myProj.csproj t:PublishAll' * publishCmdArray.push(`dotnet msbuild ${permutation[1]}`) * } * else { * publishCmdArray.push(`dotnet publish ${permutation}`) * } * } * // return array as success-chained CLI commands. * return publishCmdArray.join(' && '); */ function getPublishArgumentsPermutations(proj) { /** * If the project imports PublishAll to publish for each TFM-RID * permutation, return the appropriate command line. */ if (proj.Targets.includes("PublishAll")) return [`"${proj.Properties.MSBuildProjectFullPath}" -restore -t:PublishAll -p:Configuration=Release`]; const tfmRidPermutations = []; const RIDs = proj.Properties.RuntimeIdentifiers.split(";").filter((v) => v !== ""); const TFMs = proj.Properties.TargetFrameworks.split(";").filter((v) => v !== ""); if (TFMs.length === 0 && RIDs.length === 0) return [`"${proj.Properties.MSBuildProjectFullPath}"`]; if (RIDs.length > 0) if (TFMs.length > 0) for (const RID of RIDs) for (const TFM of TFMs) tfmRidPermutations.push(`--runtime ${RID} --framework ${TFM}`); else for (const RID of RIDs) tfmRidPermutations.push(`--runtime ${RID}`); else if (TFMs.length > 0) for (const TFM of TFMs) tfmRidPermutations.push(`--framework ${TFM}`); /** prepend each set of args with the project's path */ return tfmRidPermutations.map((permArguments) => `"${proj.Properties.MSBuildProjectFullPath}" ${permArguments}`); } const publishCmds = []; /** convert {@link evaluatedPublishProjects} to sets of space-separated CLI args. */ const argumentsSets = evaluatedPublishProjects.map((proj) => getPublishArgumentsPermutations(proj)); for (const arguments_ of argumentsSets) { if (typeof arguments_ === "string") throw new Error(`\`args\` should not be a string!`); for (const permutation of arguments_) { if (typeof permutation === "string" && permutation.length === 1) throw new Error("Something has gone terribly wrong. A `dotnet publish` argument set was split to single characters!"); if (/".+" -restore -t:PublishAll -p:Configuration=Release/.test(permutation)) publishCmds.push(`dotnet msbuild ${permutation}`); else publishCmds.push(`dotnet publish ${permutation}`); } } return publishCmds.join(" && "); } /** * @param projectsToPackAndPush a string[] or {@link NugetRegistryInfo}[]. * If a string[], the string must be the platform-dependent (not file://), * full path(s) to one or more projects with the .NET "Pack" MSBuild target. * See {@link https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-pack} * for command line usage. * @returns one or more command line strings joined with ' && '. * Each command line comprises the `dotnet pack` command, a project file path, * and a hardcoded output path (`--output ${cwd()}/publish`) */ async function formatDotnetPack(projectsToPackAndPush) { if (projectsToPackAndPush.length === 0) return void 0; return (await Promise.all(projectsToPackAndPush.map(async (proj) => { if (proj instanceof NugetRegistryInfo) return proj; const msbpArray = await Promise.all(await MSBuildProject.PackableProjectsToMSBuildProjects([proj])); if (msbpArray.length === 0 || msbpArray[0] === void 0) throw new Error("This should be impossible!"); const msbp = msbpArray[0]; evaluatedProjects.push(msbp); return new NugetRegistryInfo({ project: msbp }); }))).map((nri) => nri.GetPackCommand(NugetRegistryInfo.PackPackagesOptionsType.from({}))).join(" && "); } } /** * Prepare the CLI command to push NuGet packages. This should added to the `publishCmd` option of `@semantic-release/exec` * * Ensure your verifyConditionsCmd is set to prevent releases failing due to bad tokens or packages! * See {@link NugetRegistryInfo#PackDummyPackage}, {@link NugetRegistryInfo#GetPushDummyCommand} * @param registryInfos an array of {@link NugetRegistryInfo} (or derived classes) instances. * @param packageOutputPath Default: `${cwd()}/publish`.\ * The directory at which dotnet outputs the given projects' packages. Passed to * `dotnet pack` via the `--output` argument. * @returns a string of `dotnet pack` and `dotnet push` commands, joined by ' && '. */ function configureDotnetNugetPush(registryInfos, packageOutputPath = `${cwd()}/publish`) { if (registryInfos.some((registry) => registry.source.trim() === "")) throw new Error("The URL for one of the provided NuGet registries was empty or whitespace."); const packCmds = registryInfos.map((nri) => nri.GetPackCommand({ output: packageOutputPath }, true, true)); const pushCmds = registryInfos.map((nri) => nri.GetPushCommand({ root: packageOutputPath }, true, true)); return [...packCmds, ...pushCmds].join(" && "); } /** * You should try {@link ../../dotnet/SignAfterPack.targets}!. * @param opts A {@link DotnetNugetSignOptions} object to be deconstructed and * passed to `dotnet nuget sign` as args. * @returns `dotnet nuget sign {...}` */ function formatDotnetNugetSign(opts) { if (opts === void 0) return void 0; const validOptions = DotnetNugetSignOptions.from(opts); const arguments_ = [ "--timestamper", validOptions.timestamper, "-o", validOptions.output ?? ourDefaultPubDirectory ]; if (validOptions.certificatePassword) arguments_.push("---certificate-password", validOptions.certificatePassword); if (validOptions.hashAlgorithm) arguments_.push("--hash-algorithm", validOptions.hashAlgorithm); if (validOptions.overwrite) arguments_.push("--overwrite"); if (validOptions.timestampHashAlgorithm) arguments_.push("--timestamp-hash-algorithm", validOptions.timestampHashAlgorithm); if (validOptions.verbosity) arguments_.push("-v", validOptions.verbosity); if ("certificatePath" in validOptions) arguments_.push("--certificate-path", validOptions.certificatePath); else if ("certificateStoreName" in validOptions) { SetSubjectNameOrFingerprint(); arguments_.push("--certificate-store-name", validOptions.certificateStoreName); } else if ("certificateStoreLocation" in validOptions) { SetSubjectNameOrFingerprint(); arguments_.push("--certificate-store-location", validOptions.certificateStoreLocation); } else throw new Error("No code signing certificate was specified!"); return `dotnet nuget sign ${arguments_.join(" ")} `; function SetSubjectNameOrFingerprint() { if ("certificateSubjectName" in validOptions) arguments_.push("--certificate-subject-name", validOptions.certificateSubjectName); else if ("certificateFingerprint" in validOptions) arguments_.push("--certificate-fingerprint", validOptions.certificateFingerprint); else throw new Error("If certificateStoreName or certificateStoreLocation is set, either certificateSubjectName or certificateFingerprint must also be set!"); } } const DotnetNugetSignOptions = type({ /** * Password for the certificate, if needed. This option can be used to specify * the password for the certificate. The command will throw an error message * if certificate is password protected but password is not provided as input. */ "certificatePassword?": "string", /** * Hash algorithm to be used to sign the package. Defaults to SHA256. */ "hashAlgorithm?": "string | \"SHA256\"", /** * Directory where the signed package(s) should be saved. By default the * original package is overwritten by the signed package. */ "output?": "string", /** * Switch to indicate if the current signature should be overwritten. By * default the command will fail if the package already has a signature. */ "overwrite?": "true", /** * URL to an RFC 3161 timestamping server. */ timestamper: "string = \"https://rfc3161.ai.moda/\"", /** * Hash algorithm to be used to sign the package. Defaults to SHA256. */ "timestampHashAlgorithm?": "string | \"SHA256\"", /** * Set the verbosity level of the command. Allowed values are q[uiet], * m[inimal], n[ormal], d[etailed], and diag[nostic]. */ "verbosity?": "\"q\"|\"quiet\"|\"m\"|\"minimal\"|\"n\"|\"normal\"|\"d\"|\"detailed\"|\"diag\"|\"diagnostic\"" }).and(type({ /** * File path to the certificate to be used while signing the package. */ certificatePath: "string" }).or(type({ /** * Name of the X.509 certificate store to use to search for the * certificate. Defaults to "My", the X.509 certificate store for personal * certificates. * * This option should be used when specifying the certificate via * --certificate-subject-name or --certificate-fingerprint options. */ certificateStoreName: "string" }).or({ /** * Name of the X.509 certificate store use to search for the * certificate. Defaults to "CurrentUser", the X.509 certificate store * used by the current user. * * This option should be used when specifying the certificate via * --certificate-subject-name or --certificate-fingerprint options. */ certificateStoreLocation: "string" })).and(type({ /** * Subject name of the certificate used to search a local certificate * store for the certificate. The search is a case-insensitive string * comparison using the supplied value, which will find all certificates * with the subject name containing that string, regardless of other * subject values. The certificate store can be specified by * --certificate-store-name and --certificate-store-location options. */ certificateSubjectName: "string" }).or({ /** * SHA-256, SHA-384 or SHA-512 fingerprint of the certificate used to * search a local certificate store for the certificate. The certificate * store can be specified by --certificate-store-name and * --certificate-store-location options. */ certificateFingerprint: "string" }))); //#endregion export { configureDotnetNugetPush, configurePrepareCmd }; //# sourceMappingURL=helpers.mjs.map