/// <summary> /// Update the ModuleVersion of the bumped module in any dependent module's RequiredModule field. /// </summary> private void UpdateDependentModules() { var moduleName = _fileHelper.ModuleName; var projectDirectories = _fileHelper.ProjectDirectories; foreach (var projectDirectory in projectDirectories) { var moduleManifestPaths = Directory.GetFiles(projectDirectory, "*.psd1", SearchOption.AllDirectories) .Where(f => !f.Contains("Netcore") && !f.Contains("bin") && !f.Contains("dll-Help") && !ModuleFilter.IsAzureStackModule(f)) .ToList(); foreach (var moduleManifestPath in moduleManifestPaths) { var file = File.ReadAllLines(moduleManifestPath); var pattern = @"ModuleName(\s*)=(\s*)(['\""])" + moduleName + @"(['\""])(\s*);(\s*)ModuleVersion(\s*)=(\s*)(['\""])" + "\\d+(\\.\\d+)+" + @"(['\""])"; if (file.Where(l => Regex.IsMatch(l, pattern)).Any()) { var updatedFile = file.Select(l => Regex.Replace(l, pattern, "ModuleName = '" + moduleName + "'; ModuleVersion = '" + _newVersion + "'")); File.WriteAllLines(moduleManifestPath, updatedFile); } } } }
/// <summary> /// Update the ModuleVersion of the bumped module in any dependent module's RequiredModule field. /// </summary> private void UpdateDependentModules() { var moduleName = _fileHelper.ModuleName; var projectDirectories = _fileHelper.ProjectDirectories; var outputDirectories = _fileHelper.OutputDirectories; foreach (var projectDirectory in projectDirectories) { var moduleManifestPaths = Directory.GetFiles(projectDirectory, "*.psd1", SearchOption.AllDirectories) .Where(f => !f.Contains("Netcore") && !f.Contains("bin") && !f.Contains("dll-Help") && !ModuleFilter.IsAzureStackModule(f)) .ToList(); foreach (var moduleManifestPath in moduleManifestPaths) { var file = File.ReadAllLines(moduleManifestPath); var pattern = @"ModuleName(\s*)=(\s*)(['\""])" + moduleName + @"(['\""])(\s*);(\s*)ModuleVersion(\s*)=(\s*)(['\""])" + _oldVersion + @"(['\""])"; if (file.Where(l => Regex.IsMatch(l, pattern)).Any()) { var updatedFile = file.Select(l => Regex.Replace(l, pattern, "ModuleName = '" + moduleName + "'; ModuleVersion = '" + _newVersion + "'")); File.WriteAllLines(moduleManifestPath, updatedFile); var updatedModuleName = Path.GetFileNameWithoutExtension(moduleManifestPath); foreach (var outputDirectory in outputDirectories) { var outputModuleDirectory = Directory.GetDirectories(outputDirectory, updatedModuleName).FirstOrDefault(); if (outputModuleDirectory == null) { continue; } var outputModuleManifestPath = Directory.GetFiles(outputModuleDirectory, updatedModuleName + ".psd1").FirstOrDefault(); if (outputModuleManifestPath == null) { continue; } File.WriteAllLines(outputModuleManifestPath, updatedFile); } } } } }
/// <summary> /// Validate version bump of changed modules or a specified module. /// </summary> private static void ValidateVersionBump() { var changedModules = new List <string>(); foreach (var directory in _projectDirectories) { var changeLogs = Directory.GetFiles(directory, "ChangeLog.md", SearchOption.AllDirectories) .Where(f => !ModuleFilter.IsAzureStackModule(f)) .Select(f => GetModuleManifestPath(Directory.GetParent(f).FullName)) .Where(m => !string.IsNullOrEmpty(m) && m.Contains(_moduleNameFilter)) .ToList(); changedModules.AddRange(changeLogs); } foreach (var projectModuleManifestPath in changedModules) { var moduleFileName = Path.GetFileName(projectModuleManifestPath); var moduleName = moduleFileName.Replace(".psd1", ""); var outputModuleManifest = _outputDirectories .SelectMany(d => Directory.GetDirectories(d, moduleName)) .SelectMany(d => Directory.GetFiles(d, moduleFileName)) .ToList(); if (outputModuleManifest.Count == 0) { throw new FileNotFoundException("No module manifest file found in output directories"); } else if (outputModuleManifest.Count > 1) { throw new IndexOutOfRangeException("Multiple module manifest files found in output directories"); } var outputModuleManifestFile = outputModuleManifest.FirstOrDefault(); var validatorFileHelper = new VersionFileHelper(_rootDirectory, outputModuleManifestFile, projectModuleManifestPath); _versionValidator = new VersionValidator(validatorFileHelper); _versionValidator.ValidateAllVersionBumps(); } }
/// <summary> /// Given a set of directory paths containing PowerShell module folders, /// analyze the breaking changes in the modules and report any issues /// /// Filters can be added to find breaking changes for specific modules /// </summary> /// <param name="cmdletProbingDirs">Set of directory paths containing PowerShell module folders to be checked for breaking changes.</param> /// <param name="directoryFilter">Function that filters the directory paths to be checked.</param> /// <param name="cmdletFilter">Function that filters the cmdlets to be checked.</param> public void Analyze( IEnumerable <string> cmdletProbingDirs, Func <IEnumerable <string>, IEnumerable <string> > directoryFilter, Func <string, bool> cmdletFilter, IEnumerable <string> modulesToAnalyze) { var processedHelpFiles = new List <string>(); var issueLogger = Logger.CreateLogger <BreakingChangeIssue>("BreakingChangeIssues.csv"); if (directoryFilter != null) { cmdletProbingDirs = directoryFilter(cmdletProbingDirs); } foreach (var baseDirectory in cmdletProbingDirs.Where(s => !s.Contains("ServiceManagement") && !ModuleFilter.IsAzureStackModule(s) && Directory.Exists(Path.GetFullPath(s)))) { var probingDirectories = new List <string> { baseDirectory }; // Add current directory for probing probingDirectories.AddRange(Directory.EnumerateDirectories(Path.GetFullPath(baseDirectory))); foreach (var directory in probingDirectories) { if (modulesToAnalyze != null && modulesToAnalyze.Any() && !modulesToAnalyze.Any(m => directory.EndsWith(m))) { continue; } var service = Path.GetFileName(directory); var manifestFiles = Directory.EnumerateFiles(directory, "*.psd1").ToList(); if (manifestFiles.Count > 1) { manifestFiles = manifestFiles.Where(f => Path.GetFileName(f).IndexOf(service) >= 0).ToList(); } if (manifestFiles.Count == 0) { continue; } var psd1 = manifestFiles.FirstOrDefault(); var parentDirectory = Directory.GetParent(psd1).FullName; var psd1FileName = Path.GetFileName(psd1); var powershell = PowerShell.Create(); var script = $"Import-LocalizedData -BaseDirectory {parentDirectory} -FileName {psd1FileName} -BindingVariable ModuleMetadata;"; powershell.AddScript($"{script} $ModuleMetadata.NestedModules;"); var cmdletResult = powershell.Invoke(); var nestedModules = cmdletResult.Where(c => c != null).Select(c => c.ToString()).Select(c => (c.StartsWith(".") ? c.Substring(2) : c)).ToList(); powershell.AddScript($"{script} $ModuleMetadata.RequiredModules | % {{ $_[\"ModuleName\"] }};"); cmdletResult = powershell.Invoke(); var requiredModules = cmdletResult.Where(c => !c.ToString().StartsWith(".")).Select(c => c.ToString()).ToList(); if (!nestedModules.Any()) { continue; } Directory.SetCurrentDirectory(directory); requiredModules = requiredModules.Join(cmdletProbingDirs, module => 1, dir => 1, (module, dir) => Path.Combine(dir, module)) .Where(Directory.Exists) .ToList(); requiredModules.Add(directory); foreach (var nestedModule in nestedModules) { var assemblyFile = Directory.GetFiles(parentDirectory, nestedModule, SearchOption.AllDirectories).FirstOrDefault(); var assemblyFileName = Path.GetFileName(assemblyFile); if (!File.Exists(assemblyFile)) { continue; } issueLogger.Decorator.AddDecorator(a => a.AssemblyFileName = assemblyFileName, "AssemblyFileName"); processedHelpFiles.Add(assemblyFileName); // TODO: Remove IfDef #if NETSTANDARD var proxy = new CmdletLoader(); #else var proxy = EnvironmentHelpers.CreateProxy <CmdletLoader>(directory, out _appDomain); #endif var newModuleMetadata = proxy.GetModuleMetadata(assemblyFile, requiredModules); var fileName = assemblyFileName + ".json"; var executingPath = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).AbsolutePath); var filePath = executingPath + "\\SerializedCmdlets\\" + fileName; #if SERIALIZE SerializeCmdlets(filePath, newModuleMetadata); #endif if (!File.Exists(filePath)) { continue; } var oldModuleMetadata = DeserializeCmdlets(filePath); if (cmdletFilter != null) { var output = string.Format("Before filter\nOld module cmdlet count: {0}\nNew module cmdlet count: {1}", oldModuleMetadata.Cmdlets.Count, newModuleMetadata.Cmdlets.Count); output += string.Format("\nCmdlet file: {0}", assemblyFileName); oldModuleMetadata.FilterCmdlets(cmdletFilter); newModuleMetadata.FilterCmdlets(cmdletFilter); output += string.Format("After filter\nOld module cmdlet count: {0}\nNew module cmdlet count: {1}", oldModuleMetadata.Cmdlets.Count, newModuleMetadata.Cmdlets.Count); foreach (var cmdlet in oldModuleMetadata.Cmdlets) { output += string.Format("\n\tOld cmdlet - {0}", cmdlet.Name); } foreach (var cmdlet in newModuleMetadata.Cmdlets) { output += string.Format("\n\tNew cmdlet - {0}", cmdlet.Name); } issueLogger.WriteMessage(output + Environment.NewLine); } RunBreakingChangeChecks(oldModuleMetadata, newModuleMetadata, issueLogger); // TODO: Remove IfDef code #if !NETSTANDARD AppDomain.Unload(_appDomain); #endif } } } }
/// <summary> /// Bump the version of changed modules or a specified module. /// </summary> private static void BumpVersions() { var changedModules = new List <string>(); foreach (var directory in _projectDirectories) { var changeLogs = Directory.GetFiles(directory, "ChangeLog.md", SearchOption.AllDirectories) .Where(f => !ModuleFilter.IsAzureStackModule(f) && IsChangeLogUpdated(f)) .Select(f => GetModuleManifestPath(Directory.GetParent(f).FullName)) .Where(m => m.Contains(_moduleNameFilter)) .ToList(); changedModules.AddRange(changeLogs); } var executingAssemblyPath = Assembly.GetExecutingAssembly().Location; var versionControllerDirectory = Directory.GetParent(executingAssemblyPath).FullName; var miniVersionFile = Path.Combine(versionControllerDirectory, "MinimalVersion.csv"); if (File.Exists(miniVersionFile)) { var lines = File.ReadAllLines(miniVersionFile).Skip(1).Where(c => !string.IsNullOrEmpty(c)); foreach (var line in lines) { var cols = line.Split(",").Select(c => c.StartsWith("\"") ? c.Substring(1) : c) .Select(c => c.EndsWith("\"") ? c.Substring(0, c.Length - 1) : c) .Select(c => c.Trim()).ToArray(); if (cols.Length >= 2) { _minimalVersion.Add(cols[0], new AzurePSVersion(cols[1])); } } } foreach (var projectModuleManifestPath in changedModules) { var moduleFileName = Path.GetFileName(projectModuleManifestPath); var moduleName = moduleFileName.Replace(".psd1", ""); var outputModuleManifest = _outputDirectories .SelectMany(d => Directory.GetDirectories(d, moduleName)) .SelectMany(d => Directory.GetFiles(d, moduleFileName)) .ToList(); if (outputModuleManifest.Count == 0) { throw new FileNotFoundException("No module manifest file found in output directories"); } else if (outputModuleManifest.Count > 1) { throw new IndexOutOfRangeException("Multiple module manifest files found in output directories"); } var outputModuleManifestFile = outputModuleManifest.FirstOrDefault(); _versionBumper = new VersionBumper(new VersionFileHelper(_rootDirectory, outputModuleManifestFile, projectModuleManifestPath)); if (_minimalVersion.ContainsKey(moduleName)) { _versionBumper.MinimalVersion = _minimalVersion[moduleName]; } _versionBumper.BumpAllVersions(); } }
/// <summary> /// Given a set of directory paths containing PowerShell module folders, /// analyze the breaking changes in the modules and report any issues /// /// Filters can be added to find breaking changes for specific modules /// </summary> /// <param name="cmdletProbingDirs">Set of directory paths containing PowerShell module folders to be checked for breaking changes.</param> /// <param name="directoryFilter">Function that filters the directory paths to be checked.</param> /// <param name="cmdletFilter">Function that filters the cmdlets to be checked.</param> public void Analyze( IEnumerable <string> cmdletProbingDirs, Func <IEnumerable <string>, IEnumerable <string> > directoryFilter, Func <string, bool> cmdletFilter, IEnumerable <string> modulesToAnalyze) { var processedHelpFiles = new List <string>(); var issueLogger = Logger.CreateLogger <BreakingChangeIssue>("BreakingChangeIssues.csv"); if (directoryFilter != null) { cmdletProbingDirs = directoryFilter(cmdletProbingDirs); } foreach (var baseDirectory in cmdletProbingDirs.Where(s => !s.Contains("ServiceManagement") && !ModuleFilter.IsAzureStackModule(s) && Directory.Exists(Path.GetFullPath(s)))) { SharedAssemblyLoader.Load(baseDirectory); var probingDirectories = new List <string> { baseDirectory }; // Add current directory for probing probingDirectories.AddRange(Directory.EnumerateDirectories(Path.GetFullPath(baseDirectory))); foreach (var directory in probingDirectories) { if (modulesToAnalyze != null && modulesToAnalyze.Any() && !modulesToAnalyze.Any(m => directory.EndsWith(m))) { continue; } var service = Path.GetFileName(directory); var manifestFiles = Directory.EnumerateFiles(directory, "*.psd1").ToList(); if (manifestFiles.Count > 1) { manifestFiles = manifestFiles.Where(f => Path.GetFileName(f).IndexOf(service) >= 0).ToList(); } if (manifestFiles.Count == 0) { continue; } var psd1 = manifestFiles.FirstOrDefault(); var parentDirectory = Directory.GetParent(psd1).FullName; var psd1FileName = Path.GetFileName(psd1); string moduleName = psd1FileName.Replace(".psd1", ""); Console.WriteLine(directory); Directory.SetCurrentDirectory(directory); issueLogger.Decorator.AddDecorator(a => a.AssemblyFileName = moduleName, "AssemblyFileName"); processedHelpFiles.Add(moduleName); var newModuleMetadata = MetadataLoader.GetModuleMetadata(moduleName); var fileName = $"{moduleName}.json"; var executingPath = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).AbsolutePath); var filePath = Path.Combine(executingPath, "SerializedCmdlets", fileName); if (!File.Exists(filePath)) { continue; } var oldModuleMetadata = ModuleMetadata.DeserializeCmdlets(filePath); if (cmdletFilter != null) { var output = string.Format("Before filter\nOld module cmdlet count: {0}\nNew module cmdlet count: {1}", oldModuleMetadata.Cmdlets.Count, newModuleMetadata.Cmdlets.Count); output += string.Format("\nCmdlet file: {0}", moduleName); oldModuleMetadata.FilterCmdlets(cmdletFilter); newModuleMetadata.FilterCmdlets(cmdletFilter); output += string.Format("After filter\nOld module cmdlet count: {0}\nNew module cmdlet count: {1}", oldModuleMetadata.Cmdlets.Count, newModuleMetadata.Cmdlets.Count); foreach (var cmdlet in oldModuleMetadata.Cmdlets) { output += string.Format("\n\tOld cmdlet - {0}", cmdlet.Name); } foreach (var cmdlet in newModuleMetadata.Cmdlets) { output += string.Format("\n\tNew cmdlet - {0}", cmdlet.Name); } issueLogger.WriteMessage(output + Environment.NewLine); } RunBreakingChangeChecks(oldModuleMetadata, newModuleMetadata, issueLogger); } } }
public void Analyze(IEnumerable <string> cmdletProbingDirs, Func <IEnumerable <string>, IEnumerable <string> > directoryFilter, Func <string, bool> cmdletFilter, IEnumerable <string> modulesToAnalyze) { var savedDirectory = Directory.GetCurrentDirectory(); var processedHelpFiles = new List <string>(); var issueLogger = Logger.CreateLogger <SignatureIssue>(_signatureIssueReportLoggerName); var probingDirectories = new List <string>(); if (directoryFilter != null) { cmdletProbingDirs = directoryFilter(cmdletProbingDirs); } foreach (var baseDirectory in cmdletProbingDirs.Where(s => !s.Contains("ServiceManagement") && !ModuleFilter.IsAzureStackModule(s) && Directory.Exists(Path.GetFullPath(s)))) { SharedAssemblyLoader.Load(baseDirectory); //Add current directory for probing probingDirectories.Add(baseDirectory); probingDirectories.AddRange(Directory.EnumerateDirectories(Path.GetFullPath(baseDirectory))); foreach (var directory in probingDirectories) { if (modulesToAnalyze != null && modulesToAnalyze.Any() && !modulesToAnalyze.Any(m => directory.EndsWith(m))) { continue; } var service = Path.GetFileName(directory); var manifestFiles = Directory.EnumerateFiles(directory, "*.psd1").ToList(); if (manifestFiles.Count > 1) { manifestFiles = manifestFiles.Where(f => Path.GetFileName(f).IndexOf(service) >= 0).ToList(); } if (!manifestFiles.Any()) { continue; } var psd1 = manifestFiles.FirstOrDefault(); var parentDirectory = Directory.GetParent(psd1).FullName; var psd1FileName = Path.GetFileName(psd1); string moduleName = psd1FileName.Replace(".psd1", ""); Directory.SetCurrentDirectory(directory); issueLogger.Decorator.AddDecorator(a => a.AssemblyFileName = moduleName, "AssemblyFileName"); processedHelpFiles.Add(moduleName); var module = MetadataLoader.GetModuleMetadata(moduleName); CmdletLoader.ModuleMetadata = module; var cmdlets = module.Cmdlets; if (cmdletFilter != null) { cmdlets = cmdlets.Where(cmdlet => cmdletFilter(cmdlet.Name)).ToList(); } foreach (var cmdlet in cmdlets) { Logger.WriteMessage("Processing cmdlet '{0}'", cmdlet.ClassName); const string defaultRemediation = "Determine if the cmdlet should implement ShouldProcess and " + "if so determine if it should implement Force / ShouldContinue"; if (!cmdlet.SupportsShouldProcess && cmdlet.HasForceSwitch) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 0, problemId: SignatureProblemId.ForceWithoutShouldProcessAttribute, description: string.Format("{0} Has -Force parameter but does not set the SupportsShouldProcess " + "property to true in the Cmdlet attribute.", cmdlet.Name), remediation: defaultRemediation); } if (!cmdlet.SupportsShouldProcess && cmdlet.ConfirmImpact != ConfirmImpact.Medium) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 2, problemId: SignatureProblemId.ConfirmLeveleWithNoShouldProcess, description: string.Format("{0} Changes the ConfirmImpact but does not set the " + "SupportsShouldProcess property to true in the cmdlet attribute.", cmdlet.Name), remediation: defaultRemediation); } if (!cmdlet.SupportsShouldProcess && cmdlet.IsShouldProcessVerb) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.ActionIndicatesShouldProcess, description: string.Format( "{0} Does not support ShouldProcess but the cmdlet verb {1} indicates that it should.", cmdlet.Name, cmdlet.VerbName), remediation: defaultRemediation); } if (cmdlet.ConfirmImpact != ConfirmImpact.Medium) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 2, problemId: SignatureProblemId.ConfirmLevelChange, description: string.Format("{0} changes the confirm impact. Please ensure that the " + "change in ConfirmImpact is justified", cmdlet.Name), remediation: "Verify that ConfirmImpact is changed appropriately by the cmdlet. " + "It is very rare for a cmdlet to change the ConfirmImpact."); } if (!cmdlet.IsApprovedVerb) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.CmdletWithUnapprovedVerb, description: string.Format( "{0} uses the verb '{1}', which is not on the list of approved " + "verbs for PowerShell commands. Use the cmdlet 'Get-Verb' to see " + "the full list of approved verbs and consider renaming the cmdlet.", cmdlet.Name, cmdlet.VerbName), remediation: "Consider renaming the cmdlet to use an approved verb for PowerShell."); } if (!cmdlet.HasSingularNoun) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.CmdletWithPluralNoun, description: string.Format( "{0} uses the noun '{1}', which does not follow the enforced " + "naming convention of using a singular noun for a cmdlet name.", cmdlet.Name, cmdlet.NounName), remediation: "Consider using a singular noun for the cmdlet name."); } if (!cmdlet.OutputTypes.Any()) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.CmdletWithNoOutputType, description: string.Format( "Cmdlet '{0}' has no defined output type.", cmdlet.Name), remediation: "Add an OutputType attribute that declares the type of the object(s) returned " + "by this cmdlet. If this cmdlet returns no output, please set the output " + "type to 'bool' and make sure to implement the 'PassThru' parameter."); } foreach (var parameter in cmdlet.GetParametersWithPluralNoun()) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.ParameterWithPluralNoun, description: string.Format( "Parameter {0} of cmdlet {1} does not follow the enforced " + "naming convention of using a singular noun for a parameter name.", parameter.Name, cmdlet.Name), remediation: "Consider using a singular noun for the parameter name."); } foreach (var parameterSet in cmdlet.ParameterSets) { if (parameterSet.Name.Contains(" ")) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.ParameterSetWithSpace, description: string.Format( "Parameter set '{0}' of cmdlet '{1}' contains a space, which " + "is discouraged for PowerShell parameter sets.", parameterSet.Name, cmdlet.Name), remediation: "Remove the space(s) in the parameter set name."); } if (parameterSet.Parameters.Any(p => p.Position >= 4)) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.ParameterWithOutOfRangePosition, description: string.Format( "Parameter set '{0}' of cmdlet '{1}' contains at least one parameter " + "with a position larger than four, which is discouraged.", parameterSet.Name, cmdlet.Name), remediation: "Limit the number of positional parameters in a single parameter set to " + "four or fewer."); } } if (cmdlet.ParameterSets.Count > 2 && cmdlet.DefaultParameterSetName == "__AllParameterSets") { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.MultipleParameterSetsWithNoDefault, description: string.Format( "Cmdlet '{0}' has multiple parameter sets, but no defined default parameter set.", cmdlet.Name), remediation: "Define a default parameter set in the cmdlet attribute."); } ValidateParameterSetWithMandatoryEqual(cmdlet, issueLogger); ValidateParameterSetWithLenientMandatoryEqual(cmdlet, issueLogger); } issueLogger.Decorator.Remove("AssemblyFileName"); Directory.SetCurrentDirectory(savedDirectory); } } }
/// <summary> /// Given a set of directory paths containing PowerShell module folders, /// analyze the breaking changes in the modules and report any issues /// /// Filters can be added to find breaking changes for specific modules /// </summary> /// <param name="cmdletProbingDirs">Set of directory paths containing PowerShell module folders to be checked for breaking changes.</param> /// <param name="directoryFilter">Function that filters the directory paths to be checked.</param> /// <param name="cmdletFilter">Function that filters the cmdlets to be checked.</param> /// <param name="modulesToAnalyze">The set of modules to analyze</param> public void Analyze(IEnumerable <string> cmdletProbingDirs, Func <IEnumerable <string>, IEnumerable <string> > directoryFilter, Func <string, bool> cmdletFilter, IEnumerable <string> modulesToAnalyze) { if (directoryFilter != null) { cmdletProbingDirs = directoryFilter(cmdletProbingDirs); } var logFileName = Path.Combine(OutputFilePath, BreakingChangeAttributeReportLoggerName); //Init the log file var logger = TextFileLogger.GetTextFileLogger(logFileName, CleanBreakingChangesFileBeforeWriting); try { foreach (var baseDirectory in cmdletProbingDirs.Where(s => !s.Contains("ServiceManagement") && !ModuleFilter.IsAzureStackModule(s) && Directory.Exists(Path.GetFullPath(s)))) { var probingDirectories = new List <string> { baseDirectory }; // Add current directory for probing probingDirectories.AddRange(Directory.EnumerateDirectories(Path.GetFullPath(baseDirectory))); foreach (var directory in probingDirectories) { if (modulesToAnalyze != null && modulesToAnalyze.Any() && !modulesToAnalyze.Any(m => directory.EndsWith(m))) { continue; } var cmdlets = GetCmdletsFilesInFolder(directory); if (!cmdlets.Any()) { continue; } foreach (var cmdletFileName in cmdlets) { var cmdletFileFullPath = Path.Combine(directory, Path.GetFileName(cmdletFileName)); if (!File.Exists(cmdletFileFullPath)) { continue; } // TODO: Remove IfDef #if NETSTANDARD var proxy = new CmdletBreakingChangeAttributeLoader(); #else var proxy = EnvironmentHelpers.CreateProxy <CmdletBreakingChangeAttributeLoader>(directory, out _appDomain); #endif var cmdletDataForModule = proxy.GetModuleBreakingChangeAttributes(cmdletFileFullPath); //If there is nothing in this module just continue if (cmdletDataForModule == null) { Console.WriteLine("No breaking change attributes found in module " + cmdletFileName); continue; } if (cmdletFilter != null) { var output = string.Format("Before filter\nmodule cmdlet count: {0}\n", cmdletDataForModule.CmdletList.Count); output += string.Format("\nCmdlet file: {0}", cmdletFileFullPath); cmdletDataForModule.FilterCmdlets(cmdletFilter); output += string.Format("After filter\nmodule cmdlet count: {0}\n", cmdletDataForModule.CmdletList.Count); foreach (var cmdlet in cmdletDataForModule.CmdletList) { output += string.Format("\n\tcmdlet - {0}", cmdlet.CmdletName); } Console.WriteLine(output); } LogBreakingChangesInModule(cmdletDataForModule, logger); // TODO: Remove IfDef code #if !NETSTANDARD AppDomain.Unload(_appDomain); #endif } } } } finally { if (logger != null) { TextFileLogger.CloseLogger(logFileName); } } }
public void Analyze(IEnumerable <string> cmdletProbingDirs, Func <IEnumerable <string>, IEnumerable <string> > directoryFilter, Func <string, bool> cmdletFilter, IEnumerable <string> modulesToAnalyze) { var savedDirectory = Directory.GetCurrentDirectory(); var processedHelpFiles = new List <string>(); var issueLogger = Logger.CreateLogger <SignatureIssue>(_signatureIssueReportLoggerName); var probingDirectories = new List <string>(); if (directoryFilter != null) { cmdletProbingDirs = directoryFilter(cmdletProbingDirs); } foreach (var baseDirectory in cmdletProbingDirs.Where(s => !s.Contains("ServiceManagement") && !ModuleFilter.IsAzureStackModule(s) && Directory.Exists(Path.GetFullPath(s)))) { //Add current directory for probing probingDirectories.Add(baseDirectory); probingDirectories.AddRange(Directory.EnumerateDirectories(Path.GetFullPath(baseDirectory))); foreach (var directory in probingDirectories) { if (modulesToAnalyze != null && modulesToAnalyze.Any() && !modulesToAnalyze.Any(m => directory.EndsWith(m))) { continue; } var service = Path.GetFileName(directory); var manifestFiles = Directory.EnumerateFiles(directory, "*.psd1").ToList(); if (manifestFiles.Count > 1) { manifestFiles = manifestFiles.Where(f => Path.GetFileName(f).IndexOf(service) >= 0).ToList(); } if (!manifestFiles.Any()) { continue; } var psd1 = manifestFiles.FirstOrDefault(); var parentDirectory = Directory.GetParent(psd1).FullName; var psd1FileName = Path.GetFileName(psd1); var powershell = PowerShell.Create(); var script = $"Import-LocalizedData -BaseDirectory {parentDirectory} -FileName {psd1FileName} -BindingVariable ModuleMetadata;"; powershell.AddScript($"{script} $ModuleMetadata.NestedModules;"); var cmdletResult = powershell.Invoke(); var nestedModules = cmdletResult.Where(c => c != null).Select(c => c.ToString()).Select(c => (c.StartsWith(".") ? c.Substring(2) : c)).ToList(); powershell.AddScript($"{script} $ModuleMetadata.RequiredModules | % {{ $_[\"ModuleName\"] }};"); cmdletResult = powershell.Invoke(); var requiredModules = cmdletResult.Where(c => !c.ToString().StartsWith(".")).Select(c => c.ToString()).ToList(); if (!nestedModules.Any()) { continue; } Directory.SetCurrentDirectory(directory); requiredModules = requiredModules.Join(cmdletProbingDirs, module => 1, dir => 1, (module, dir) => Path.Combine(dir, module)) .Where(Directory.Exists) .ToList(); requiredModules.Add(directory); foreach (var nestedModule in nestedModules) { var assemblyFile = Directory.GetFiles(parentDirectory, nestedModule, SearchOption.AllDirectories).FirstOrDefault(); if (!File.Exists(assemblyFile)) { continue; } issueLogger.Decorator.AddDecorator(a => a.AssemblyFileName = assemblyFile, "AssemblyFileName"); processedHelpFiles.Add(assemblyFile); // TODO: Remove IfDef #if NETSTANDARD var proxy = new CmdletLoader(); #else var proxy = EnvironmentHelpers.CreateProxy <CmdletLoader>(directory, out _appDomain); #endif var module = proxy.GetModuleMetadata(assemblyFile, requiredModules); var cmdlets = module.Cmdlets; if (cmdletFilter != null) { cmdlets = cmdlets.Where(cmdlet => cmdletFilter(cmdlet.Name)).ToList(); } foreach (var cmdlet in cmdlets) { Logger.WriteMessage("Processing cmdlet '{0}'", cmdlet.ClassName); const string defaultRemediation = "Determine if the cmdlet should implement ShouldProcess and " + "if so determine if it should implement Force / ShouldContinue"; if (!cmdlet.SupportsShouldProcess && cmdlet.HasForceSwitch) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 0, problemId: SignatureProblemId.ForceWithoutShouldProcessAttribute, description: string.Format("{0} Has -Force parameter but does not set the SupportsShouldProcess " + "property to true in the Cmdlet attribute.", cmdlet.Name), remediation: defaultRemediation); } if (!cmdlet.SupportsShouldProcess && cmdlet.ConfirmImpact != ConfirmImpact.Medium) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 2, problemId: SignatureProblemId.ConfirmLeveleWithNoShouldProcess, description: string.Format("{0} Changes the ConfirmImpact but does not set the " + "SupportsShouldProcess property to true in the cmdlet attribute.", cmdlet.Name), remediation: defaultRemediation); } if (!cmdlet.SupportsShouldProcess && cmdlet.IsShouldProcessVerb) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.ActionIndicatesShouldProcess, description: string.Format( "{0} Does not support ShouldProcess but the cmdlet verb {1} indicates that it should.", cmdlet.Name, cmdlet.VerbName), remediation: defaultRemediation); } if (cmdlet.ConfirmImpact != ConfirmImpact.Medium) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 2, problemId: SignatureProblemId.ConfirmLevelChange, description: string.Format("{0} changes the confirm impact. Please ensure that the " + "change in ConfirmImpact is justified", cmdlet.Name), remediation: "Verify that ConfirmImpact is changed appropriately by the cmdlet. " + "It is very rare for a cmdlet to change the ConfirmImpact."); } if (!cmdlet.IsApprovedVerb) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.CmdletWithUnapprovedVerb, description: string.Format( "{0} uses the verb '{1}', which is not on the list of approved " + "verbs for PowerShell commands. Use the cmdlet 'Get-Verb' to see " + "the full list of approved verbs and consider renaming the cmdlet.", cmdlet.Name, cmdlet.VerbName), remediation: "Consider renaming the cmdlet to use an approved verb for PowerShell."); } if (!cmdlet.HasSingularNoun) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.CmdletWithPluralNoun, description: string.Format( "{0} uses the noun '{1}', which does not follow the enforced " + "naming convention of using a singular noun for a cmdlet name.", cmdlet.Name, cmdlet.NounName), remediation: "Consider using a singular noun for the cmdlet name."); } if (!cmdlet.OutputTypes.Any()) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.CmdletWithNoOutputType, description: string.Format( "Cmdlet '{0}' has no defined output type.", cmdlet.Name), remediation: "Add an OutputType attribute that declares the type of the object(s) returned " + "by this cmdlet. If this cmdlet returns no output, please set the output " + "type to 'bool' and make sure to implement the 'PassThru' parameter."); } foreach (var parameter in cmdlet.GetParametersWithPluralNoun()) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.ParameterWithPluralNoun, description: string.Format( "Parameter {0} of cmdlet {1} does not follow the enforced " + "naming convention of using a singular noun for a parameter name.", parameter.Name, cmdlet.Name), remediation: "Consider using a singular noun for the parameter name."); } foreach (var parameterSet in cmdlet.ParameterSets) { if (parameterSet.Name.Contains(" ")) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.ParameterSetWithSpace, description: string.Format( "Parameter set '{0}' of cmdlet '{1}' contains a space, which " + "is discouraged for PowerShell parameter sets.", parameterSet.Name, cmdlet.Name), remediation: "Remove the space(s) in the parameter set name."); } if (parameterSet.Parameters.Any(p => p.Position >= 4)) { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.ParameterWithOutOfRangePosition, description: string.Format( "Parameter set '{0}' of cmdlet '{1}' contains at least one parameter " + "with a position larger than four, which is discouraged.", parameterSet.Name, cmdlet.Name), remediation: "Limit the number of positional parameters in a single parameter set to " + "four or fewer."); } } if (cmdlet.ParameterSets.Count > 2 && cmdlet.DefaultParameterSetName == "__AllParameterSets") { issueLogger.LogSignatureIssue( cmdlet: cmdlet, severity: 1, problemId: SignatureProblemId.MultipleParameterSetsWithNoDefault, description: string.Format( "Cmdlet '{0}' has multiple parameter sets, but no defined default parameter set.", cmdlet.Name), remediation: "Define a default parameter set in the cmdlet attribute."); } } // TODO: Remove IfDef code #if !NETSTANDARD AppDomain.Unload(_appDomain); #endif issueLogger.Decorator.Remove("AssemblyFileName"); } Directory.SetCurrentDirectory(savedDirectory); } } }
/// <summary> /// Bump the version of changed modules or a specified module. /// </summary> private static void BumpVersions() { string targetRepositories = null; using (PowerShell powershell = PowerShell.Create()) { powershell.AddScript("Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process;"); powershell.AddScript("Register-PackageSource -Name PSGallery -Location https://www.powershellgallery.com/api/v2 -ProviderName PowerShellGet"); powershell.AddScript("Register-PackageSource -Name TestGallery -Location https://www.poshtestgallery.com/api/v2 -ProviderName PowerShellGet"); powershell.AddScript("Get-PSRepository"); var repositories = powershell.Invoke(); string psgallery = null; string testgallery = null; foreach (var repo in repositories) { if ("https://www.powershellgallery.com/api/v2".Equals(repo.Properties["SourceLocation"]?.Value)) { psgallery = repo.Properties["Name"]?.Value?.ToString(); } if ("https://www.poshtestgallery.com/api/v2".Equals(repo.Properties["SourceLocation"]?.Value)) { testgallery = repo.Properties["Name"]?.Value?.ToString(); } } if (psgallery == null) { throw new Exception("Cannot calculate module version because PSGallery is not available."); } targetRepositories = psgallery; if (testgallery == null) { Console.WriteLine("Warning: Cannot calculate module version precisely because TestGallery is not available."); } else { targetRepositories += $",{testgallery}"; } } var changedModules = new List <string>(); foreach (var directory in _projectDirectories) { var changeLogs = Directory.GetFiles(directory, "ChangeLog.md", SearchOption.AllDirectories) .Where(f => !ModuleFilter.IsAzureStackModule(f) && IsChangeLogUpdated(f)) .Select(f => GetModuleManifestPath(Directory.GetParent(f).FullName)) .Where(m => m.Contains(_moduleNameFilter)) .ToList(); changedModules.AddRange(changeLogs); } var executingAssemblyPath = Assembly.GetExecutingAssembly().Location; var versionControllerDirectory = Directory.GetParent(executingAssemblyPath).FullName; var miniVersionFile = Path.Combine(versionControllerDirectory, "MinimalVersion.csv"); if (File.Exists(miniVersionFile)) { var lines = File.ReadAllLines(miniVersionFile).Skip(1).Where(c => !string.IsNullOrEmpty(c)); foreach (var line in lines) { var cols = line.Split(",").Select(c => c.StartsWith("\"") ? c.Substring(1) : c) .Select(c => c.EndsWith("\"") ? c.Substring(0, c.Length - 1) : c) .Select(c => c.Trim()).ToArray(); if (cols.Length >= 2) { _minimalVersion.Add(cols[0], new AzurePSVersion(cols[1])); } } } //Make Az.Accounts as the first module to calcuate changedModules = changedModules.OrderBy(c => c == "Az.Accounts" ? "" : c).ToList(); foreach (var projectModuleManifestPath in changedModules) { var moduleFileName = Path.GetFileName(projectModuleManifestPath); var moduleName = moduleFileName.Replace(".psd1", ""); var outputModuleManifest = _outputDirectories .SelectMany(d => Directory.GetDirectories(d, moduleName)) .SelectMany(d => Directory.GetFiles(d, moduleFileName)) .ToList(); if (outputModuleManifest.Count == 0) { throw new FileNotFoundException("No module manifest file found in output directories"); } else if (outputModuleManifest.Count > 1) { throw new IndexOutOfRangeException("Multiple module manifest files found in output directories"); } var outputModuleManifestFile = outputModuleManifest.FirstOrDefault(); _versionBumper = new VersionBumper(new VersionFileHelper(_rootDirectory, outputModuleManifestFile, projectModuleManifestPath)); _versionBumper.PSRepositories = targetRepositories; if (_minimalVersion.ContainsKey(moduleName)) { _versionBumper.MinimalVersion = _minimalVersion[moduleName]; } _versionBumper.BumpAllVersions(); } }