Private/Build/Compare-BrownserveRepository.ps1
|
<#
.SYNOPSIS Checks the current state of a repository to see if it is initialised correctly. .DESCRIPTION This cmdlet is pretty complex as we use it to test the state of a given repository on disk to see if it is correctly initialised depending on the type of project the repository houses. We don't want to modify any files on disk until we're sure we won't destroy any manual changes that may have been made. As such this cmdlet will compare the state of the files in the repository to a set of templates that we generate based on the type of project the repository houses, if the repository is missing any files or if the files are different to that of the templates then we'll add them to a list of files that need to be created or updated and return them to the calling process to be handled. Due to the complexities of comparing files with line endings and formatting we make heavy use of the various "*-BrownserveContent" cmdlets to ensure that we can accurately compare the files. #> function Compare-BrownserveRepository { [CmdletBinding()] param ( # The path to the repository [Parameter(Mandatory = $true, Position = 0)] [string] $RepositoryPath, # The owner of the repository [Parameter(Mandatory = $false)] [string] $Owner = 'Brownserve', # The type of build that should be installed in this repo [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [BrownserveRepoProjectType] $ProjectType = 'generic', # The PowerShell module metadata, required when ProjectType is 'PowerShellModule' or 'BrownservePSTools' [Parameter(Mandatory = $false)] [BrownservePowerShellModule] $ModuleInfo, # The GitHub repository name, if different from the local directory name. # Defaults to the leaf name of RepositoryPath if not provided. [Parameter(Mandatory = $false)] [string] $RepoName, # Forces the recreation of files even if they already exist [Parameter(Mandatory = $false)] [switch] $Force, # The config file to use for setting our .gitignore content [Parameter(Mandatory = $false, DontShow)] [string] $GitIgnoreConfigFile = (Join-Path $Script:BrownservePSBuildToolsConfigDirectory 'gitignore_config.json'), # The config file to use for setting our .gitignore content [Parameter(Mandatory = $false, DontShow)] [string] $PaketDependenciesConfigFile = (Join-Path $Script:BrownservePSBuildToolsConfigDirectory 'paket_dependencies_config.json'), # The config file to use that stores our permanent/ephemeral path configuration [Parameter(Mandatory = $false, DontShow)] [string] $RepositoryPathsConfigFile = (Join-Path $Script:BrownservePSBuildToolsConfigDirectory 'repository_paths_config.json'), # The config file that stores devcontainer configurations [Parameter(Mandatory = $false, DontShow)] [string] $DevcontainerConfigFile = (Join-Path $Script:BrownservePSBuildToolsConfigDirectory 'devcontainer_config.json'), # The config file that stores VS Code extension configuration [Parameter(Mandatory = $false, DontShow)] [string] $VSCodeExtensionsConfigFile = (Join-Path $Script:BrownservePSBuildToolsConfigDirectory 'repository_vscode_extensions.json'), # The config file that stores any package aliases we'd like to create [Parameter(Mandatory = $false, DontShow)] [string] $PackageAliasConfigFile = (Join-Path $Script:BrownservePSBuildToolsConfigDirectory 'package_aliases_config.json'), # The config file that stores any editorconfig settings we'd like to create [Parameter(Mandatory = $false, DontShow)] [string] $EditorConfigConfigFile = (Join-Path $Script:BrownservePSBuildToolsConfigDirectory 'editorconfig_config.json'), # The config file that stores markdownlint settings [Parameter(Mandatory = $false, DontShow)] [string] $MarkdownlintConfigFile = (Join-Path $Script:BrownservePSBuildToolsConfigDirectory 'markdownlint_config.json') ) begin { # Ensure that dotnet is available for us to use, we need it to instal tooling and make our nuget.config try { $RequiredTools = @('dotnet') Write-Verbose 'Checking for required tooling' Assert-Command $RequiredTools # Just having dotnet isn't enough, we need to ensure at least one SDK is installed # Otherwise `dotnet new` won't work $DotNetSDKs = & dotnet --list-sdks if (-not $DotNetSDKs) { throw "No .NET SDKs are installed. Please install a .NET SDK to continue." } } catch { throw "$($_.Exception.Message)`nThese tools are required to configure a Brownserve repository." } # Ensure the config files are valid try { $GitIgnoreConfig = Read-ConfigurationFromFile $GitIgnoreConfigFile $PaketDependenciesConfig = Read-ConfigurationFromFile $PaketDependenciesConfigFile $RepositoryPathsConfig = Read-ConfigurationFromFile $RepositoryPathsConfigFile $DevcontainerConfig = Read-ConfigurationFromFile $DevcontainerConfigFile $PackageAliasConfig = Read-ConfigurationFromFile $PackageAliasConfigFile # Load VS code extensions as a hashtable so we can easily merge things later on $VSCodeExtensionsConfig = Read-ConfigurationFromFile $VSCodeExtensionsConfigFile -AsHashtable # Load EditorConfig as a hashtable as our [EditorConfigSection] type cannot process psobject's $EditorConfigConfig = Read-ConfigurationFromFile $EditorConfigConfigFile -AsHashtable $MarkdownlintConfig = Read-ConfigurationFromFile $MarkdownlintConfigFile } catch { throw "Failed to import configuration data.`n$($_.Exception.Message)" } } process { # Ensure we have a valid repository path Assert-Directory $RepositoryPath -ErrorAction 'Stop' <# The point of this cmdlet is to check the state of a given repository and ensure it's configured correctly. Therefore we can end up in a few different states: - The repository is already configured correctly - The repository is missing some files - Some managed files exist but require updating/changing - Some managed files already exist but are in a format we can't parse (likely manually created or modified) #> $UnParsableFiles = @() $MissingFiles = @() $ChangedFiles = @() $MissingDirectories = @() $IncludeChangelog = $false $IncludeWorkflows = $false $IncludeMarkdownlint = $false $IncludeDependabot = $false $IncludeLabelPR = $false $IncludeBuildScripts = $false $IncludeHelpTests = $false $BuildScriptUseWorkingCopyOption = $false <# We type constrain these variables to ensure that we can easily add to them later on. In the past certain operations had a tendency to return a single string rather than an array of strings Which would cause issues when we converted them to JSON. #> [array]$VSCodeWorkspaceExtensionIDs = @() $VSCodeWorkspaceSettings = [ordered]@{} <# Our config file contains a list of permanent paths that should always be created in a repository. They survive between init's and are not gitignored. #> $DefaultPermanentPaths = $RepositoryPathsConfig.Defaults.PermanentPaths <# Our config file may contain a list of ephemeral paths that get created when the _init script is run. They are deleted between init's and are commonly gitignored. #> $DefaultEphemeralPaths = $RepositoryPathsConfig.Defaults.EphemeralPaths if ($DefaultPermanentPaths.VariableName -notcontains 'BrownserveRepoBuildDirectory') { throw 'BrownserveRepoBuildDirectory path not found in repository paths config file.' #TODO: Should we consider raising this as a warning instead? } $BuildDirectory = Join-Path $RepositoryPath ($DefaultPermanentPaths | Where-Object { $_.VariableName -eq 'BrownserveRepoBuildDirectory' }).Path <# The below paths will always be required regardless of the type of repository we're working with. #> $ManifestPath = Join-Path $RepositoryPath '.brownserve_repository_manifest' $InitPath = Join-Path $BuildDirectory '_init.ps1' $PaketDependenciesPath = Join-Path $RepositoryPath 'paket.dependencies' $dotnetToolsConfigPath = Join-Path $RepositoryPath '.config' $dotnetToolsPath = Join-Path $dotnetToolsConfigPath 'dotnet-tools.json' $NugetConfigPath = Join-Path $RepositoryPath 'nuget.config' $GitIgnorePath = Join-Path $RepositoryPath '.gitignore' # These paths may or may not be required depending on the type of repository we're working with $VSCodePath = Join-Path $RepositoryPath '.vscode' $VSCodeExtensionsFilePath = Join-Path $VSCodePath 'extensions.json' $VSCodeWorkspaceSettingsFilePath = Join-Path $VSCodePath 'settings.json' $DevcontainerDirectoryPath = Join-Path $RepositoryPath '.devcontainer' $DevcontainerPath = Join-Path $DevcontainerDirectoryPath 'devcontainer.json' $DockerfilePath = Join-Path $DevcontainerDirectoryPath 'Dockerfile' $EditorConfigPath = Join-Path $RepositoryPath '.editorconfig' $MarkdownlintConfigPath = Join-Path $RepositoryPath '.markdownlint.json' $ChangelogPath = Join-Path $RepositoryPath 'CHANGELOG.md' $LicensePath = Join-Path $RepositoryPath 'LICENSE' $GitHubDirectory = Join-Path $RepositoryPath '.github' $WorkflowDirectory = Join-Path $GitHubDirectory 'workflows' $BuildDirectory = Join-Path $RepositoryPath '.build' $BuildTasksDirectory = Join-Path $BuildDirectory 'tasks' <# To help with consistency we store a special manifest file in the repository that contains some basic information about the repository. (Right now we just use it to store the type of repository we're working with.) #> if ((Test-Path $ManifestPath)) { Write-Verbose "Found existing repository manifest file at '$ManifestPath'" try { $CurrentManifest = Get-Content -Path $ManifestPath -ErrorAction 'Stop' | ConvertFrom-Json -Depth 100 -AsHashtable } catch { throw "Failed to read repository manifest file.`n$($_.Exception.Message)" } # Check to see if the repository type is the same as the one we're trying to configure, if it's not # then fail unless -Force has been passed. if (($CurrentManifest.RepositoryType -ne $ProjectType) -and !$Force) { throw "Repository type mismatch. Expected '$ProjectType' but repository was previously configured as '$($CurrentManifest.RepositoryType)'.`nUse the '-Force' switch to overwrite the existing configuration." } # Fail if the repository type is not present in the manifest file if (!$CurrentManifest.RepositoryType) { throw 'Repository type not found in manifest file.' } Write-Debug "Repository type found in manifest file: $($CurrentManifest.RepositoryType)" } <# Because we don't want to make any changes to the repository until we're sure we can do so safely, we'll create a temporary directory to stage all our files in. #> try { $TempDir = New-BrownserveTemporaryDirectory } catch { throw "Failed to create temporary directory.`n$($_.Exception.Message)" } <# We often recommend the use of various VS Code extensions with our projects. There may already be some settings in the repo as well, we should try and preserve those as best we can. N.B If the extensions.json file only contains a single key then it will be read as a string rather than an array so we use the += operator to ensure that we always end up adding any items to the array we created above. #> try { $VSCodeWorkspaceExtensionIDs += Get-VSCodeWorkspaceExtensions -WorkspacePath $RepositoryPath -ErrorAction 'Stop' } catch [BrownserveFileNotFound] { <# Repo probably doesn't have the extensions.json file yet Don't terminate as this is expected behaviour. #> Write-Verbose 'No VS Code extensions.json file found, using the empty array' } catch { throw "Failed to get existing recommended extensions.`n$($_.Exception.Message)" } try { $VSCodeWorkspaceSettings = Get-VSCodeWorkspaceSettings -WorkspacePath $RepositoryPath -ErrorAction 'Stop' } catch [BrownserveFileNotFound] { Write-Verbose 'No VS Code settings.json file found, using the empty dictionary' <# Repo probably doesn't have the settings.json file yet, We'll use the empty dictionary we created above. #> } catch { throw "Failed to get existing VS Code settings.`n$($_.Exception.Message)" } <# Check for the presence of any managed files in the repo, they may already exist if this repo has been configured before. They may contain manual entries that we should try and preserve. However it's also entirely possible that these files were created before we started using this cmdlet and they may not be in a format we can parse, if this is the case we'll add them to the list of unparsable files. #> if (Test-Path $GitIgnorePath) { Write-Verbose 'Parsing existing .gitignore file.' try { $CurrentGitIgnores = Get-BrownserveContent -Path $GitIgnorePath -ErrorAction 'Stop' $ManualGitIgnores = $CurrentGitIgnores | Select-BrownserveContent -After '## Manually defined ignores: ##' -FailIfNotFound } catch { $UnParsableFiles += $GitIgnorePath } } # Similarly for paket packages if (Test-Path $PaketDependenciesPath) { Write-Verbose 'Parsing existing paket.dependencies file.' try { $ManualPaketEntries = Get-BrownserveContent -Path $PaketDependenciesPath | Select-BrownserveContent -After '## Manually defined dependencies: ##' -FailIfNotFound } catch { $UnParsableFiles += $PaketDependenciesPath } } # And for any custom _init.ps1 steps if (Test-Path $InitPath) { Write-Verbose 'Parsing existing _init.ps1 file.' try { $CurrentInitContent = Get-BrownserveContent -Path $InitPath -ErrorAction 'Stop' $CustomInitSteps = $CurrentInitContent | Select-BrownserveContent ` -After '### Start user defined _init steps' ` -Before '### End user defined _init steps' ` -FailIfNotFound } catch { $UnParsableFiles += $InitPath } } # Build up our default list of gitignore's that we always want to use # TODO: Do we want to make ignoring paket.lock optional? $DefaultGitIgnores = $GitIgnoreConfig.Defaults # Set-up the paket dependency that are common to all our projects $DefaultPaketDependencies = $PaketDependenciesConfig.Defaults # Careful -AsHashtable makes key names case sensitive when converted from JSON! (defaults != Defaults) $DefaultVSCodeExtensions = $VSCodeExtensionsConfig.Defaults $DefaultPackageAliases = $PackageAliasConfig.Defaults $DefaultEditorConfig = $EditorConfigConfig.Defaults # We don't use a config file to create the manifest file as it's a simple object $NewManifest = [System.Management.Automation.OrderedHashtable]@{ RepositoryType = $ProjectType.ToString() ManifestVersion = '1.0.0' } switch ($ProjectType) { <# For a repo that houses a PowerShell module we'll want to include: - The logic for loading the module as part of the _init script - PlatyPS for building module documentation - powershell-yaml for working with CI/CD files - Invoke-Build/Pester for building and testing the module #> 'PowerShellModule' { Write-Debug 'PowerShell Module selected' # Check our configuration files for any special logic when working with PowerShell module repos $DockerfileName = $DevcontainerConfig.PowerShellModule.Dockerfile $ExtraPermanentPaths = $RepositoryPathsConfig.PowerShellModule.PermanentPaths $ExtraEphemeralPaths = $RepositoryPathsConfig.PowerShellModule.EphemeralPaths $ExtraPaketDeps = $PaketDependenciesConfig.PowerShellModule $ExtraGitIgnores = $GitIgnoreConfig.PowerShellModule $ExtraVSCodeExtensions = $VSCodeExtensionsConfig.PowerShellModule $ExtraPackageAliases = $PackageAliasConfig.PowerShellModule $ExtraEditorConfig = $EditorConfigConfig.PowerShellModule $IncludeChangelog = $true $InitParams = @{ IncludeModuleLoader = $true IncludePowerShellYaml = $true IncludePlatyPS = $true IncludeBuildTestTools = $true } $LicenseType = 'MIT' $IncludeWorkflows = $true $IncludeMarkdownlint = $true $IncludeDependabot = $true $IncludeLabelPR = $true $IncludeBuildScripts = $true $IncludeHelpTests = $true $IncludeMkDocs = $true $DependabotParams = @{ Updates = @( @{ Ecosystem = 'github-actions'; Directory = '/'; Interval = 'weekly' }, @{ Ecosystem = 'nuget'; Directory = '/.config'; Interval = 'weekly' } ) } } <# For the repo that houses this very PowerShell module we want to do things a little differently. We avoid loading the Brownserve.PSTools module locally in _init.ps1 and use nuget as normal to get a stable version (this ensures that we can still get notified of failed builds) We can use our build to load the local version of the module. #> 'BrownservePSTools' { Write-Debug 'BrownservePSTools selected' # For now we use the same basic config as all our other PowerShell modules except in the params below $DockerfileName = $DevcontainerConfig.PowerShellModule.Dockerfile $ExtraPermanentPaths = $RepositoryPathsConfig.PowerShellModule.PermanentPaths $ExtraEphemeralPaths = $RepositoryPathsConfig.PowerShellModule.EphemeralPaths $ExtraPaketDeps = $PaketDependenciesConfig.PowerShellModule $ExtraGitIgnores = $GitIgnoreConfig.PowerShellModule $ExtraVSCodeExtensions = $VSCodeExtensionsConfig.PowerShellModule $ExtraPackageAliases = $PackageAliasConfig.PowerShellModule $ExtraEditorConfig = $EditorConfigConfig.PowerShellModule $IncludeChangelog = $true $InitParams = @{ IncludeModuleLoader = $false # we don't want to load the module locally, we want the stable version from nuget IncludePowerShellYaml = $true IncludePlatyPS = $true IncludeBuildTestTools = $true } $IncludeWorkflows = $true $IncludeMarkdownlint = $true $IncludeDependabot = $true $IncludeLabelPR = $true $IncludeBuildScripts = $true $IncludeHelpTests = $true $IncludeMkDocs = $true $BuildScriptUseWorkingCopyOption = $true $DependabotParams = @{ Updates = @( @{ Ecosystem = 'github-actions'; Directory = '/'; Interval = 'weekly' }, @{ Ecosystem = 'nuget'; Directory = '/.config'; Interval = 'weekly' } ) } } Default { Write-Debug 'Generic project type selected' # We always need the $InitParams hashtable otherwise we'll get a null-valued expression error $InitParams = @{ IncludeModuleLoader = $false IncludePowerShellYaml = $false IncludePlatyPS = $false IncludeBuildTestTools = $false } } } if ($IncludeWorkflows -and -not $ModuleInfo) { throw "A '-ModuleInfo' value is required for '$ProjectType' repositories." } if ($UnParsableFiles.Count -gt 0 -and !$Force) { { <# Throw here, this allows us to give the user a list of files that need to be manually checked. Then the user can either modify the files themselves or pass -Force to this cmdlet to overwrite them. #> throw "The following files already exist in the repository but are in a format that can't be parsed:`n$($UnParsableFiles -join "`n")" } } if ($DockerfileName) { $DevcontainerParams = @{ Dockerfile = $DockerfileName RequiredExtensions = @() } } if ($ExtraPermanentPaths) { $FinalPermanentPaths = $DefaultPermanentPaths + $ExtraPermanentPaths } else { $FinalPermanentPaths = $DefaultPermanentPaths } if ($ExtraEphemeralPaths.Count -gt 0) { $FinalEphemeralPaths = $DefaultEphemeralPaths + $ExtraEphemeralPaths } else { $FinalEphemeralPaths = $DefaultEphemeralPaths } $InitParams.Add('PermanentPaths', $FinalPermanentPaths) $InitParams.Add('EphemeralPaths', $FinalEphemeralPaths) if ($ExtraGitIgnores) { $FinalGitIgnores = $DefaultGitIgnores + $ExtraGitIgnores } else { $FinalGitIgnores = $DefaultGitIgnores } if ($ExtraPackageAliases) { $FinalPackageAliases = $DefaultPackageAliases + $ExtraPackageAliases } else { $FinalPackageAliases = $DefaultPackageAliases } if ($FinalPackageAliases) { $InitParams.Add('PackageAliases', $FinalPackageAliases) } $GitIgnoreParams = @{ GitIgnores = $FinalGitIgnores } if ($ManualGitIgnores) { $GitIgnoreParams.Add('ManualGitIgnores', $ManualGitIgnores) } if ($ExtraPaketDeps) { $FinalPaketDependencies = $DefaultPaketDependencies + $ExtraPaketDeps } else { $FinalPaketDependencies = $DefaultPaketDependencies } $PaketParams = @{ PaketDependencies = $FinalPaketDependencies } if ($ManualPaketEntries) { $PaketParams.Add('ManualDependencies', $ManualPaketEntries) } if ($ExtraEditorConfig) { $FinalEditorConfig = $DefaultEditorConfig + $ExtraEditorConfig } else { $FinalEditorConfig = $DefaultEditorConfig } $EditorConfigParams = @{ IncludeRoot = $true Section = $FinalEditorConfig } if ($CustomInitSteps) { $InitParams.Add('CustomInitSteps', $CustomInitSteps) } if ($ExtraVSCodeExtensions) { $VSCodeExtensions = $DefaultVSCodeExtensions + $ExtraVSCodeExtensions } else { $VSCodeExtensions = $DefaultVSCodeExtensions } if ($VSCodeExtensions.Count -gt 0) { # Extract the list of extension ID's we want to install in this repo and clean up any duplicates $VSCodeWorkspaceExtensionIDs += $VSCodeExtensions.ExtensionID $VSCodeWorkspaceExtensionIDs = $VSCodeWorkspaceExtensionIDs | Select-Object -Unique <# Due to the way we store the VS Code settings in our config file, they end up getting read out as an array when we expand the object property. However the cmdlet that creates the settings file expects a hashtable. By far the easiest method to convert this to a hashtable is to pass our array of Hashtable's to the Merge-Hashtable cmdlet as the InputObject with a blank hashtable as the BaseObject. This results in a hashtable being returned with the correct key/value pairs. We specify the -Deep parameter so a deep merge is performed, this ensures that any settings that already exist in the repo are preserved. #> try { $VSCodeExtensionSettings = Merge-Hashtable ` -BaseObject @{} ` -InputObject $VSCodeExtensions.CustomSettings ` -Deep ` -ErrorAction 'Stop' } catch { throw "Failed to convert VS Code extension settings to hashtable.`n$($_.Exception.Message)" } <# Check to see if the repository already has any VS Code settings - it affects the order of the hash merge Our Merge-Hashtable cmdlet will overwrite the keys of the base object with the input object if there is a clash if -Force has been passed then the user is happy to overwrite any settings that already exist in the repo. If not we should try and preserve them by using the repo settings as the input object #> if ($VSCodeWorkspaceSettings.Count -gt 0) { $MergeParams = @{ BaseObject = $VSCodeWorkspaceSettings InputObject = $VSCodeExtensionSettings } if (!$Force) { $MergeParams = @{ BaseObject = $VSCodeExtensionSettings InputObject = $VSCodeWorkspaceSettings } } try { $VSCodeWorkspaceSettings = Merge-Hashtable ` @MergeParams ` -Deep ` -ErrorAction 'Stop' } catch { throw "Failed to merge repository VS code settings.`n$($_.Exception.Message)" } } else { $VSCodeWorkspaceSettings = $VSCodeExtensionSettings } <# Once we've merged the settings we like to ensure that they are sorted alphabetically. This ensures that the settings file is easier to read and also makes it easier to spot any discrepancies. #> $VSCodeWorkspaceSettings = ConvertTo-SortedHashtable $VSCodeWorkspaceSettings } # Create the _init script as that will always be required try { $NewInitScriptContent = New-BrownserveInitScript @InitParams -ErrorAction 'Stop' } catch { throw "Failed to generate _init.ps1 content.`n$($_.Exception.Message)" } # The .gitignore file should always be required too try { $NewGitIgnoresContent = New-GitIgnoresFile @GitIgnoreParams -ErrorAction 'Stop' } catch { throw "Failed to generate .gitignore file.`n$($_.Exception.Message)" } # Again the nuget.config file will always be needed try { Invoke-NativeCommand ` -FilePath 'dotnet' ` -ArgumentList 'new', 'nugetconfig' ` -WorkingDirectory $TempDir ` -SuppressOutput $NugetConfigTempPath = Join-Path $TempDir 'nuget.config' if (!(Test-Path $NugetConfigTempPath)) { Write-Error 'Cannot find staging nuget.config file.' } } catch { throw "Failed to generate nuget.config.`n$($_.Exception.Message)" } # As will the dotnet tools manifest try { <# Newer .NET SDK versions create dotnet-tools.json in the working directory root rather than in a .config/ subdirectory, so we look at the root of the temp dir for the staging file. The destination in the actual repo remains at .config/dotnet-tools.json, which is the documented standard location that dotnet tool restore checks. #> $dotnetToolsTempPath = Join-Path $TempDir 'dotnet-tools.json' Invoke-NativeCommand ` -FilePath 'dotnet' ` -ArgumentList 'new', 'tool-manifest' ` -WorkingDirectory $TempDir ` -SuppressOutput Invoke-NativeCommand ` -FilePath 'dotnet' ` -ArgumentList 'tool', 'install', 'Paket' ` -WorkingDirectory $TempDir ` -SuppressOutput if (!(Test-Path $dotnetToolsTempPath)) { Write-Error 'Cannot find staging dotnet tools manifest.' } } catch { throw "Failed to generate dotnet tools manifest.`n$($_.Exception.Message)" } # Paket may or may not be required if ($PaketParams) { try { $NewPaketDependenciesContent = New-PaketDependenciesFile @PaketParams -ErrorAction 'Stop' } catch { throw "Failed to generate paket.dependencies file.`n$($_.Exception.Message)" } } if ($DevcontainerParams) { $DevcontainerParams.RequiredExtensions = $VSCodeWorkspaceExtensionIDs try { $Devcontainer = New-VSCodeDevContainer @DevcontainerParams -ErrorAction 'Stop' } catch { throw "Failed to create devcontainer.`n$($_.Exception.Message)" } } if ($EditorConfigParams) { # Try to preserve any manual changes that may have been made to the editorconfig file if (Test-Path $EditorConfigPath) { try { $ManualEditorConfig = Read-BrownserveEditorConfig -Path $EditorConfigPath -ErrorAction 'Stop' } catch { # Let this silently fail and just try and create the editorconfig anyways # (If we've got here then -Force has been passed so we should overwrite any existing editorconfig file) } } if ($ManualEditorConfig) { $EditorConfigParams.Add('ManualSection', $ManualEditorConfig) } try { $NewEditorConfigContent = New-BrownserveEditorConfig @EditorConfigParams -ErrorAction 'Stop' } catch { throw "Failed to create .editorconfig file content.`n$($_.Exception.Message)" } } $FinalPermanentPaths.GetEnumerator() | ForEach-Object { <# All paths should be relative to the repository root. The entry may contain child paths, hopefully the user has defined them in the correct order so that the parent always gets created first! #> if ($_.ChildPaths) { $JoinPathParams = @{ Path = $RepositoryPath ChildPath = $_.Path AdditionalChildPath = $_.ChildPaths } } else { $JoinPathParams = @{ Path = $RepositoryPath ChildPath = $_.Path } } $PathToCheck = Join-Path @JoinPathParams if (!(Test-Path $PathToCheck)) { $MissingDirectories += [pscustomobject]@{ Path = $PathToCheck } } } # The type of license we use is dependent on the type of project we're working with # though in the future we may want to allow the user to override this. if ($LicenseType) { $NewLicenseContent = New-SPDXLicense ` -LicenseType $LicenseType ` -Owner $Owner ` -ErrorAction 'Stop' | Format-BrownserveContent } <# Now that we've generated all the files for the repository we will compare them to any existing files in the repo. If the content matches then we don't need to do anything. If there's a difference then we'll add the file to the list of changed files. If the files don't exist at all then we'll add them to the list of missing files. We set the SyncWindow to 1 to try and make the comparison more readable. This should ensure that adding a new line to the template file would result in a single addition being reported against the DifferenceObject rather than an addition against the DifferenceObject and a removal against the ReferenceObject. Similarly if a line is removed from the template file we should only see a single removal against the ReferenceObject rather than a removal against the ReferenceObject and an addition against the DifferenceObject. Unfortunately there is no way to detect unexpected changes to any files already in the repo as there's no way to tell if the changes are the result of the template changing or if they were made manually. But any manual changes should be picked up by the user in the VCS diff. #> try { $NewManifestJSON = ConvertTo-Json $NewManifest -Depth 100 -ErrorAction 'Stop' | Format-BrownserveContent $CurrentManifestJSON = ConvertTo-Json $CurrentManifest -Depth 100 -ErrorAction 'Stop' | Format-BrownserveContent if ($CurrentManifest) { Write-Verbose 'Checking for changes to repository manifest' $ManifestCompare = Compare-Object ` -ReferenceObject $CurrentManifestJSON.Content ` -DifferenceObject $NewManifestJSON.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($ManifestCompare) { Write-Verbose 'Changes detected in repository manifest' $ChangedFiles += [pscustomobject]@{ Path = $ManifestPath Content = $NewManifestJSON.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing repository manifest found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $ManifestPath Content = $NewManifestJSON.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$ManifestPath'.`n$($_.Exception.Message)" } try { $NewNugetConfig = Get-BrownserveContent -Path $NugetConfigTempPath -ErrorAction 'Stop' if ((Test-Path $NugetConfigPath)) { Write-Verbose 'Checking for changes to nuget.config' $CurrentNugetConfig = Get-BrownserveContent -Path $NugetConfigPath -ErrorAction 'Stop' $NugetConfigCompare = Compare-Object ` -ReferenceObject $CurrentNugetConfig.Content ` -DifferenceObject $NewNugetConfig.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($NugetConfigCompare) { Write-Verbose 'Changes detected in nuget.config' $ChangedFiles += [pscustomobject]@{ Path = $NugetConfigPath Content = $NewNugetConfig.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing nuget.config found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $NugetConfigPath Content = $NewNugetConfig.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$NugetConfigPath'.`n$($_.Exception.Message)" } <# We don't perform any modifications to the dotnet tools manifest, so we'll just test for it's existence #> if (!(Test-Path $dotnetToolsConfigPath) -and ($MissingDirectories.Path -notcontains $dotnetToolsConfigPath)) { $MissingDirectories += [pscustomobject]@{ Path = $dotnetToolsConfigPath } } if (!(Test-Path $dotnetToolsPath)) { try { $dotnetToolsContent = Get-Content $dotnetToolsTempPath -ErrorAction 'Stop' } catch { throw "Failed to read dotnet-tools.json content.`n$($_.Exception.Message)" } Write-Verbose 'No existing dotnet-tools.json found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $dotnetToolsPath Content = $dotnetToolsContent LineEnding = 'LF' } } if ($CurrentInitContent) { Write-Verbose 'Checking for changes to _init.ps1' $InitCompare = Compare-Object ` -ReferenceObject $CurrentInitContent.Content ` -DifferenceObject $NewInitScriptContent.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($InitCompare) { Write-Verbose 'Changes detected in _init.ps1' $ChangedFiles += [pscustomobject]@{ Path = $InitPath Content = $NewInitScriptContent.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing _init.ps1 found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $InitPath Content = $NewInitScriptContent.Content LineEnding = 'LF' } } if ($CurrentGitIgnores) { Write-Verbose 'Checking for changes to .gitignore' $GitIgnoreCompare = Compare-Object ` -ReferenceObject $CurrentGitIgnores.Content ` -DifferenceObject $NewGitIgnoresContent.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($GitIgnoreCompare) { Write-Verbose 'Changes detected in .gitignore' $ChangedFiles += [pscustomobject]@{ Path = $GitIgnorePath Content = $NewGitIgnoresContent.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing .gitignore found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $GitIgnorePath Content = $NewGitIgnoresContent.Content LineEnding = 'LF' } } # Ensure the VS Code directory exists if (!(Test-Path $VSCodePath) -and ($MissingDirectories.Path -notcontains $VSCodePath)) { $MissingDirectories += [pscustomobject]@{ Path = $VSCodePath } } try { $VSCodeWorkspaceExtensionIDsJSON = ConvertTo-Json ` -InputObject @{ recommendations = $VSCodeWorkspaceExtensionIDs } ` -Depth 100 ` -ErrorAction 'Stop' | Format-BrownserveContent if ((Test-Path $VSCodeExtensionsFilePath)) { Write-Verbose 'Checking for changes to VS Code extensions.json' $CurrentVSCodeExtensions = Get-BrownserveContent -Path $VSCodeExtensionsFilePath -ErrorAction 'Stop' $VSCodeExtensionsCompare = Compare-Object ` -ReferenceObject $CurrentVSCodeExtensions.Content ` -DifferenceObject $VSCodeWorkspaceExtensionIDsJSON.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($VSCodeExtensionsCompare) { Write-Verbose 'Changes detected in VS Code extensions.json' $ChangedFiles += [pscustomobject]@{ Path = $VSCodeExtensionsFilePath Content = $VSCodeWorkspaceExtensionIDsJSON.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing extensions.json found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $VSCodeExtensionsFilePath Content = $VSCodeWorkspaceExtensionIDsJSON.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$VSCodeExtensionsFilePath'.`n$($_.Exception.Message)" } try { $VSCodeWorkspaceSettingsJSON = ConvertTo-Json ` -InputObject $VSCodeWorkspaceSettings ` -Depth 100 ` -ErrorAction 'Stop' | Format-BrownserveContent if ((Test-Path $VSCodeWorkspaceSettingsFilePath)) { Write-Verbose 'Checking for changes to VS Code settings.json' $CurrentVSCodeWorkspaceSettings = Get-BrownserveContent -Path $VSCodeWorkspaceSettingsFilePath -ErrorAction 'Stop' $VSCodeWorkspaceSettingsCompare = Compare-Object ` -ReferenceObject $CurrentVSCodeWorkspaceSettings.Content ` -DifferenceObject $VSCodeWorkspaceSettingsJSON.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($VSCodeWorkspaceSettingsCompare) { Write-Verbose 'Changes detected in VS Code settings.json' $ChangedFiles += [pscustomobject]@{ Path = $VSCodeWorkspaceSettingsFilePath Content = $VSCodeWorkspaceSettingsJSON.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing settings.json found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $VSCodeWorkspaceSettingsFilePath Content = $VSCodeWorkspaceSettingsJSON.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$VSCodeWorkspaceSettingsFilePath'.`n$($_.Exception.Message)" } if ($NewPaketDependenciesContent) { try { if ((Test-Path $PaketDependenciesPath)) { Write-Verbose 'Checking for changes to paket.dependencies' $CurrentPaketDependencies = Get-BrownserveContent -Path $PaketDependenciesPath -ErrorAction 'Stop' $PaketDependenciesCompare = Compare-Object ` -ReferenceObject $CurrentPaketDependencies.Content ` -DifferenceObject $NewPaketDependenciesContent.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($PaketDependenciesCompare) { Write-Verbose 'Changes detected in paket.dependencies' $ChangedFiles += [pscustomobject]@{ Path = $PaketDependenciesPath Content = $NewPaketDependenciesContent.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing paket.dependencies found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $PaketDependenciesPath Content = $NewPaketDependenciesContent.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$PaketDependenciesPath'.`n$($_.Exception.Message)" } } if ($Devcontainer) { try { # Devcontainer can't exist if the parent directory doesn't exist! if ((Test-Path $DevcontainerDirectoryPath)) { if ((Test-Path $DevcontainerPath)) { Write-Verbose 'Checking for changes to devcontainer.json' $CurrentDevcontainer = Get-BrownserveContent -Path $DevcontainerPath -ErrorAction 'Stop' $DevcontainerCompare = Compare-Object ` -ReferenceObject $CurrentDevcontainer.Content ` -DifferenceObject $Devcontainer.Devcontainer.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($DevcontainerCompare) { Write-Verbose 'Changes detected in devcontainer.json' $ChangedFiles += [pscustomobject]@{ Path = $DevcontainerPath Content = $Devcontainer.Devcontainer.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing devcontainer.json found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $DevcontainerPath Content = $Devcontainer.Devcontainer.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing .devcontainer directory found, will create a new one.' $MissingDirectories += [pscustomobject]@{ Path = $DevcontainerDirectoryPath } $MissingFiles += [pscustomobject]@{ Path = $DevcontainerPath Content = $Devcontainer.Devcontainer.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$DevcontainerDirectoryPath'" } try { if ((Test-Path $DockerfilePath)) { Write-Verbose 'Checking for changes to Dockerfile' $CurrentDockerfile = Get-BrownserveContent -Path $DockerfilePath -ErrorAction 'Stop' $DockerfileCompare = Compare-Object ` -ReferenceObject $CurrentDockerfile.Content ` -DifferenceObject $Devcontainer.Dockerfile.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($DockerfileCompare) { $ChangedFiles += [pscustomobject]@{ Path = $DockerfilePath Content = $Devcontainer.Dockerfile.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing Dockerfile found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $DockerfilePath Content = $Devcontainer.Dockerfile.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$DockerfilePath'.`n$($_.Exception.Message)" } } if ($NewEditorConfigContent) { try { if ((Test-Path $EditorConfigPath)) { try { Write-Verbose 'Checking for changes to .editorconfig' $CurrentEditorConfig = Get-BrownserveContent -Path $EditorConfigPath -ErrorAction 'Stop' $EditorConfigCompare = Compare-Object ` -ReferenceObject $CurrentEditorConfig.Content ` -DifferenceObject $NewEditorConfigContent.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($EditorConfigCompare) { Write-Verbose 'Changes detected in .editorconfig' $ChangedFiles += [pscustomobject]@{ Path = $EditorConfigPath Content = $NewEditorConfigContent.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$EditorConfigPath'.`n$($_.Exception.Message)" } } else { Write-Verbose 'No existing .editorconfig found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $EditorConfigPath Content = $NewEditorConfigContent.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$EditorConfigPath'.`n$($_.Exception.Message)" } } <# For the changelog we only create it if it doesn't already exist. We never overwrite it as it contains manual release notes. #> if ($IncludeChangelog) { if (!(Test-Path $ChangelogPath)) { try { $NewChangelogContent = New-BrownserveChangelogHeader -ErrorAction 'Stop' | Format-BrownserveContent } catch { throw "Failed to generate CHANGELOG.md content.`n$($_.Exception.Message)" } Write-Verbose 'No existing CHANGELOG.md found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $ChangelogPath Content = $NewChangelogContent.Content LineEnding = 'LF' } } } <# We don't ever want to overwrite the license file if it already exists, any changes to the license file should be made manually for legal reasons. #> if ($LicenseType) { if (!(Test-Path $LicensePath)) { $MissingFiles += [pscustomobject]@{ Path = $LicensePath Content = $NewLicenseContent.Content LineEnding = 'LF' } } } <# We deliberately overwrite any existing .markdownlint.json. We want a consistent gold standard across all our repos rather than per-repo drift, so any local customisations will be lost on the next init/update. #> if ($IncludeMarkdownlint) { try { $NewMarkdownlintContent = ConvertTo-Json ` -InputObject $MarkdownlintConfig ` -Depth 100 ` -ErrorAction 'Stop' | Format-BrownserveContent if ((Test-Path $MarkdownlintConfigPath)) { Write-Verbose 'Checking for changes to .markdownlint.json' $CurrentMarkdownlintContent = Get-BrownserveContent -Path $MarkdownlintConfigPath -ErrorAction 'Stop' $MarkdownlintCompare = Compare-Object ` -ReferenceObject $CurrentMarkdownlintContent.Content ` -DifferenceObject $NewMarkdownlintContent.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($MarkdownlintCompare) { Write-Verbose 'Changes detected in .markdownlint.json' $ChangedFiles += [pscustomobject]@{ Path = $MarkdownlintConfigPath Content = $NewMarkdownlintContent.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing .markdownlint.json found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $MarkdownlintConfigPath Content = $NewMarkdownlintContent.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$MarkdownlintConfigPath'.`n$($_.Exception.Message)" } } if ($ModuleInfo) { $ModuleInfoPath = Join-Path $BuildDirectory 'ModuleInfo.json' try { $ModuleInfoMap = [ordered]@{ Name = $ModuleInfo.Name Description = $ModuleInfo.Description GUID = $ModuleInfo.GUID Tags = $ModuleInfo.Tags } if ($ModuleInfo.RequiredModules) { $ModuleInfoMap.RequiredModules = $ModuleInfo.RequiredModules } $NewModuleInfoContent = $ModuleInfoMap | ConvertTo-Json -Depth 100 -ErrorAction 'Stop' | Format-BrownserveContent if (Test-Path $ModuleInfoPath) { Write-Verbose 'Checking for changes to ModuleInfo.json' $CurrentModuleInfoContent = Get-BrownserveContent -Path $ModuleInfoPath -ErrorAction 'Stop' $ModuleInfoCompare = Compare-Object ` -ReferenceObject $CurrentModuleInfoContent.Content ` -DifferenceObject $NewModuleInfoContent.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($ModuleInfoCompare) { Write-Verbose 'Changes detected in ModuleInfo.json' $ChangedFiles += [pscustomobject]@{ Path = $ModuleInfoPath Content = $NewModuleInfoContent.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing ModuleInfo.json found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $ModuleInfoPath Content = $NewModuleInfoContent.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$ModuleInfoPath'.`n$($_.Exception.Message)" } } if ($IncludeWorkflows) { if (-not $RepoName) { $RepoName = Split-Path $RepositoryPath -Leaf } $BuildsWorkflowPath = Join-Path $WorkflowDirectory 'builds.yaml' $StageReleaseWorkflowPath = Join-Path $WorkflowDirectory 'stage-release.yaml' $ReleaseWorkflowPath = Join-Path $WorkflowDirectory 'release.yaml' $WorkflowCommonParams = @{ ModuleName = $ModuleInfo.Name; RepoName = $RepoName } try { $NewBuildsWorkflowContent = New-BrownserveGitHubBuildsWorkflow -ModuleName $ModuleInfo.Name -RepoName $RepoName | Format-BrownserveContent $NewStageReleaseWorkflowContent = New-BrownserveGitHubStageReleaseWorkflow @WorkflowCommonParams | Format-BrownserveContent $NewReleaseWorkflowContent = New-BrownserveGitHubReleaseWorkflow @WorkflowCommonParams | Format-BrownserveContent } catch { throw "Failed to generate GitHub Actions workflow content.`n$($_.Exception.Message)" } if (!(Test-Path $GitHubDirectory) -and ($MissingDirectories.Path -notcontains $GitHubDirectory)) { $MissingDirectories += [pscustomobject]@{ Path = $GitHubDirectory } } if (!(Test-Path $WorkflowDirectory) -and ($MissingDirectories.Path -notcontains $WorkflowDirectory)) { $MissingDirectories += [pscustomobject]@{ Path = $WorkflowDirectory } } $WorkflowFiles = @( @{ Path = $BuildsWorkflowPath; Content = $NewBuildsWorkflowContent }, @{ Path = $StageReleaseWorkflowPath; Content = $NewStageReleaseWorkflowContent }, @{ Path = $ReleaseWorkflowPath; Content = $NewReleaseWorkflowContent } ) foreach ($WorkflowFile in $WorkflowFiles) { try { if (Test-Path $WorkflowFile.Path) { Write-Verbose "Checking for changes to '$($WorkflowFile.Path)'" $CurrentWorkflowContent = Get-BrownserveContent -Path $WorkflowFile.Path -ErrorAction 'Stop' $WorkflowCompare = Compare-Object ` -ReferenceObject $CurrentWorkflowContent.Content ` -DifferenceObject $WorkflowFile.Content.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($WorkflowCompare) { Write-Verbose "Changes detected in '$($WorkflowFile.Path)'" $ChangedFiles += [pscustomobject]@{ Path = $WorkflowFile.Path Content = $WorkflowFile.Content.Content LineEnding = 'LF' } } } else { Write-Verbose "No existing workflow file found at '$($WorkflowFile.Path)', will create a new one." $MissingFiles += [pscustomobject]@{ Path = $WorkflowFile.Path Content = $WorkflowFile.Content.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$($WorkflowFile.Path)'.`n$($_.Exception.Message)" } } } if ($IncludeLabelPR) { $LabelPRWorkflowPath = Join-Path $WorkflowDirectory 'label-pr.yaml' if (!(Test-Path $GitHubDirectory) -and ($MissingDirectories.Path -notcontains $GitHubDirectory)) { $MissingDirectories += [pscustomobject]@{ Path = $GitHubDirectory } } if (!(Test-Path $WorkflowDirectory) -and ($MissingDirectories.Path -notcontains $WorkflowDirectory)) { $MissingDirectories += [pscustomobject]@{ Path = $WorkflowDirectory } } try { $NewLabelPRWorkflowContent = New-BrownserveGitHubLabelPRWorkflow | Format-BrownserveContent if (Test-Path $LabelPRWorkflowPath) { Write-Verbose "Checking for changes to '$LabelPRWorkflowPath'" $CurrentLabelPRWorkflowContent = Get-BrownserveContent -Path $LabelPRWorkflowPath -ErrorAction 'Stop' $LabelPRCompare = Compare-Object ` -ReferenceObject $CurrentLabelPRWorkflowContent.Content ` -DifferenceObject $NewLabelPRWorkflowContent.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($LabelPRCompare) { Write-Verbose "Changes detected in '$LabelPRWorkflowPath'" $ChangedFiles += [pscustomobject]@{ Path = $LabelPRWorkflowPath Content = $NewLabelPRWorkflowContent.Content LineEnding = 'LF' } } } else { Write-Verbose "No existing label-pr.yaml found, will create a new one." $MissingFiles += [pscustomobject]@{ Path = $LabelPRWorkflowPath Content = $NewLabelPRWorkflowContent.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$LabelPRWorkflowPath'.`n$($_.Exception.Message)" } } if ($IncludeBuildScripts) { $BuildScriptPath = Join-Path $BuildDirectory 'build.ps1' $BuildTasksScriptPath = Join-Path $BuildTasksDirectory 'build_tasks.ps1' $BuildScriptParams = @{} if ($BuildScriptUseWorkingCopyOption) { $BuildScriptParams['IncludeUseWorkingCopyOption'] = $true } try { $NewBuildScriptContent = New-BrownserveBuildScript @BuildScriptParams | Format-BrownserveContent $NewBuildTasksScriptContent = New-BrownserveBuildTasksScript @BuildScriptParams | Format-BrownserveContent } catch { throw "Failed to generate build script content.`n$($_.Exception.Message)" } if (!(Test-Path $BuildDirectory) -and ($MissingDirectories.Path -notcontains $BuildDirectory)) { $MissingDirectories += [pscustomobject]@{ Path = $BuildDirectory } } if (!(Test-Path $BuildTasksDirectory) -and ($MissingDirectories.Path -notcontains $BuildTasksDirectory)) { $MissingDirectories += [pscustomobject]@{ Path = $BuildTasksDirectory } } $BuildFiles = @( @{ Path = $BuildScriptPath; Content = $NewBuildScriptContent }, @{ Path = $BuildTasksScriptPath; Content = $NewBuildTasksScriptContent } ) foreach ($BuildFile in $BuildFiles) { try { if (Test-Path $BuildFile.Path) { Write-Verbose "Checking for changes to '$($BuildFile.Path)'" $CurrentBuildFileContent = Get-BrownserveContent -Path $BuildFile.Path -ErrorAction 'Stop' $BuildFileCompare = Compare-Object ` -ReferenceObject $CurrentBuildFileContent.Content ` -DifferenceObject $BuildFile.Content.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($BuildFileCompare) { Write-Verbose "Changes detected in '$($BuildFile.Path)'" $ChangedFiles += [pscustomobject]@{ Path = $BuildFile.Path Content = $BuildFile.Content.Content LineEnding = 'LF' } } } else { Write-Verbose "No existing build file found at '$($BuildFile.Path)', will create a new one." $MissingFiles += [pscustomobject]@{ Path = $BuildFile.Path Content = $BuildFile.Content.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$($BuildFile.Path)'.`n$($_.Exception.Message)" } } } if ($IncludeHelpTests) { $BuildTestsDirectory = Join-Path $BuildDirectory 'tests' $HelpTestsPath = Join-Path $BuildTestsDirectory 'Help.Tests.ps1' try { $NewHelpTestsContent = New-BrownserveHelpTestsScript -ModuleName $ModuleInfo.Name | Format-BrownserveContent } catch { throw "Failed to generate Help.Tests.ps1 content.`n$($_.Exception.Message)" } if (!(Test-Path $BuildTestsDirectory) -and ($MissingDirectories.Path -notcontains $BuildTestsDirectory)) { $MissingDirectories += [pscustomobject]@{ Path = $BuildTestsDirectory } } try { if (Test-Path $HelpTestsPath) { Write-Verbose "Checking for changes to '$HelpTestsPath'" $CurrentHelpTestsContent = Get-BrownserveContent -Path $HelpTestsPath -ErrorAction 'Stop' $HelpTestsCompare = Compare-Object ` -ReferenceObject $CurrentHelpTestsContent.Content ` -DifferenceObject $NewHelpTestsContent.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($HelpTestsCompare) { Write-Verbose "Changes detected in '$HelpTestsPath'" $ChangedFiles += [pscustomobject]@{ Path = $HelpTestsPath Content = $NewHelpTestsContent.Content LineEnding = 'LF' } } } else { Write-Verbose "No existing Help.Tests.ps1 found, will create a new one." $MissingFiles += [pscustomobject]@{ Path = $HelpTestsPath Content = $NewHelpTestsContent.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$HelpTestsPath'.`n$($_.Exception.Message)" } } if ($IncludeMkDocs) { $PagesDirectory = Join-Path $RepositoryPath 'pages' $PagesReferenceDirectory = Join-Path $PagesDirectory 'Cmdlet reference' foreach ($Dir in @($PagesDirectory, $PagesReferenceDirectory)) { if (!(Test-Path $Dir) -and ($MissingDirectories.Path -notcontains $Dir)) { $MissingDirectories += [pscustomobject]@{ Path = $Dir } } } try { $NewMkDocsConfigContent = New-MkDocsConfig -ModuleName $ModuleInfo.Name | Format-BrownserveContent $NewMkDocsRequirementsContent = New-MkDocsRequirements | Format-BrownserveContent $NewMkDocsIndexContent = New-MkDocsIndexPage -ModuleName $ModuleInfo.Name | Format-BrownserveContent $NewMkDocsPagesContent = New-MkDocsPagesFile | Format-BrownserveContent } catch { throw "Failed to generate MkDocs file content.`n$($_.Exception.Message)" } $MkDocsFiles = @( @{ Path = (Join-Path $RepositoryPath 'mkdocs.yml'); Content = $NewMkDocsConfigContent }, @{ Path = (Join-Path $RepositoryPath 'requirements.txt'); Content = $NewMkDocsRequirementsContent }, @{ Path = (Join-Path $PagesDirectory 'index.md'); Content = $NewMkDocsIndexContent }, @{ Path = (Join-Path $PagesReferenceDirectory '.pages'); Content = $NewMkDocsPagesContent } ) foreach ($MkDocsFile in $MkDocsFiles) { try { if (Test-Path $MkDocsFile.Path) { Write-Verbose "Checking for changes to '$($MkDocsFile.Path)'" $CurrentMkDocsContent = Get-BrownserveContent -Path $MkDocsFile.Path -ErrorAction 'Stop' $MkDocsCompare = Compare-Object ` -ReferenceObject $CurrentMkDocsContent.Content ` -DifferenceObject $MkDocsFile.Content.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($MkDocsCompare) { Write-Verbose "Changes detected in '$($MkDocsFile.Path)'" $ChangedFiles += [pscustomobject]@{ Path = $MkDocsFile.Path Content = $MkDocsFile.Content.Content LineEnding = 'LF' } } } else { Write-Verbose "No existing file found at '$($MkDocsFile.Path)', will create a new one." $MissingFiles += [pscustomobject]@{ Path = $MkDocsFile.Path Content = $MkDocsFile.Content.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$($MkDocsFile.Path)'.`n$($_.Exception.Message)" } } } } end { # Return an object that contains all the information we've gathered if ($IncludeDependabot) { $DependabotGitHubDirectory = Join-Path $RepositoryPath '.github' $DependabotPath = Join-Path $DependabotGitHubDirectory 'dependabot.yml' # Ensure .github exists; guard against duplicates when $IncludeWorkflows has already added it if (!(Test-Path $DependabotGitHubDirectory) -and ($MissingDirectories.Path -notcontains $DependabotGitHubDirectory)) { $MissingDirectories += [pscustomobject]@{ Path = $DependabotGitHubDirectory } } try { $NewDependabotContent = New-BrownserveDependabotConfig @DependabotParams | Format-BrownserveContent if (Test-Path $DependabotPath) { Write-Verbose 'Checking for changes to dependabot.yml' $CurrentDependabotContent = Get-BrownserveContent -Path $DependabotPath -ErrorAction 'Stop' $DependabotCompare = Compare-Object ` -ReferenceObject $CurrentDependabotContent.Content ` -DifferenceObject $NewDependabotContent.Content ` -SyncWindow 1 ` -ErrorAction 'Stop' if ($DependabotCompare) { Write-Verbose 'Changes detected in dependabot.yml' $ChangedFiles += [pscustomobject]@{ Path = $DependabotPath Content = $NewDependabotContent.Content LineEnding = 'LF' } } } else { Write-Verbose 'No existing dependabot.yml found, will create a new one.' $MissingFiles += [pscustomobject]@{ Path = $DependabotPath Content = $NewDependabotContent.Content LineEnding = 'LF' } } } catch { throw "Failed to process '$DependabotPath'.`n$($_.Exception.Message)" } } $Return = [pscustomobject]@{ MissingFiles = $MissingFiles ChangedFiles = $ChangedFiles MissingDirectories = $MissingDirectories } Return $Return } } |