@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
JavaScript
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