From 8fd180b776c4903fb9cff90a58d25acc40748a61 Mon Sep 17 00:00:00 2001 From: OCram85 Date: Wed, 13 Jul 2022 13:59:25 +0200 Subject: [PATCH] initial migration --- .drone.yml | 122 +++++++++++ .editorconfig | 27 +++ .gitattributes | 21 ++ .gitea/ISSUE_TEMPLATE.md | 11 + .gitea/ISSUE_TEMPLATE/Bug.md | 48 +++++ .gitea/ISSUE_TEMPLATE/Enhancement.md | 24 +++ .gitea/PULL_REQUEST_TEMPLATE.md | 19 ++ .gitignore | 14 ++ .gitlocal | 19 ++ .vscode/cSpell.json | 22 ++ .vscode/dictionaries/default.txt | 0 .vscode/extensions.json | 10 + .vscode/launch.json | 48 +++++ .vscode/pwsh.code-snippets | 31 +++ .vscode/settings.json | 62 ++++++ .vscode/tasks.json | 196 ++++++++++++++++++ README.md | 193 ++++++++++++++++- assets/.gitkeep | 1 + bin/.gitkeep | 1 + build/.gitkeep | 1 + resources/.gitkeep | 0 resources/PSScriptAnalyzerSettings.psd1.back | 141 +++++++++++++ resources/TestData/encodingTest.txt | 9 + src/Build/Merge-ModuleRoot.Tests.ps1 | 21 ++ src/Build/Merge-ModuleRoot.ps1 | 42 ++++ src/Build/New-BuildPackage.Tests.ps1 | 21 ++ src/Build/New-BuildPackage.ps1 | 74 +++++++ src/Build/Update-ModuleMeta.Tests.ps1 | 21 ++ src/Build/Update-ModuleMeta.ps1 | 75 +++++++ src/CHANGELOG.md | 3 + src/Changelog/Update-Changelog.Tests.ps1 | 21 ++ src/Changelog/Update-Changelog.ps1 | 93 +++++++++ src/Deploy/Invoke-Publish.Tests.ps1 | 21 ++ src/Deploy/Invoke-Publish.ps1 | 46 ++++ src/Deps/Install-ModuleDependency.Tests.ps1 | 32 +++ src/Deps/Install-ModuleDependency.ps1 | 74 +++++++ src/Deps/Invoke-InstallDependency.Tests.ps1 | 21 ++ src/Deps/Invoke-InstallDependency.ps1 | 72 +++++++ src/Docs/New-Docs.Tests.ps1 | 21 ++ src/Docs/New-Docs.ps1 | 50 +++++ src/Docs/Update-Docs.Tests.ps1 | 21 ++ src/Docs/Update-Docs.ps1 | 49 +++++ src/DroneHelper.psd1 | 190 +++++++++++++++++ src/FileLinter/Invoke-FileLinter.Tests.ps1 | 81 ++++++++ src/FileLinter/Invoke-FileLinter.ps1 | 115 ++++++++++ src/FileLinter/Test-FileBOM.Tests.ps1 | 37 ++++ src/FileLinter/Test-FileBOM.ps1 | 61 ++++++ src/FileLinter/Test-FileEOF.Tests.ps1 | 37 ++++ src/FileLinter/Test-FileEOF.ps1 | 56 +++++ src/FileLinter/Test-FileEOL.Tests.ps1 | 38 ++++ src/FileLinter/Test-FileEOL.ps1 | 61 ++++++ src/FileLinter/Test-FileEncoding.Tests.ps1 | 41 ++++ src/FileLinter/Test-FileEncoding.ps1 | 73 +++++++ src/FileLinter/Test-FileTab.Tests.ps1 | 36 ++++ src/FileLinter/Test-FileTab.ps1 | 56 +++++ .../Test-FileTailingWhitespace.Tests.ps1 | 36 ++++ src/FileLinter/Test-FileTailingWhitespace.ps1 | 62 ++++++ src/Formats/.gitkeep | 1 + src/Helper/Get-RepoPath.Tests.ps1 | 21 ++ src/Helper/Get-RepoPath.ps1 | 167 +++++++++++++++ src/Helper/Set-EOL.Tests.ps1 | 21 ++ src/Helper/Set-EOL.ps1 | 59 ++++++ src/ModuleRoot.psm1 | 6 + src/PRComment/Send-PRComment.Tests.ps1 | 41 ++++ src/PRComment/Send-PRComment.ps1 | 173 ++++++++++++++++ src/PSModule.Tests.ps1 | 14 ++ src/PSScriptAnalyzer/Invoke-Linter.Tests.ps1 | 21 ++ src/PSScriptAnalyzer/Invoke-Linter.ps1 | 71 +++++++ src/Pester/Invoke-UnitTest.Tests.ps1 | 21 ++ src/Pester/Invoke-UnitTest.ps1 | 110 ++++++++++ src/Reports/Format-FileLinterReport.ps1 | 43 ++++ src/Reports/Format-PesterReport.ps1 | 74 +++++++ src/Reports/Format-ScriptAnalyzerReport.ps1 | 40 ++++ src/Rules/PSScriptAnalyzerSettings.psd1 | 140 +++++++++++++ src/State/Invoke-BuidState.ps1 | 27 +++ src/State/Invoke-BuildState.Tests.ps1 | 21 ++ src/State/Write-FailureStateFile.Tests.ps1 | 21 ++ src/State/Write-FailureStateFile.ps1 | 48 +++++ src/State/Write-RestultFile.Tests.ps1 | 21 ++ src/State/Write-ResultFile.ps1 | 59 ++++++ 80 files changed, 3997 insertions(+), 1 deletion(-) create mode 100644 .drone.yml create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitea/ISSUE_TEMPLATE.md create mode 100644 .gitea/ISSUE_TEMPLATE/Bug.md create mode 100644 .gitea/ISSUE_TEMPLATE/Enhancement.md create mode 100644 .gitea/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 .gitlocal create mode 100644 .vscode/cSpell.json create mode 100644 .vscode/dictionaries/default.txt create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/pwsh.code-snippets create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 assets/.gitkeep create mode 100644 bin/.gitkeep create mode 100644 build/.gitkeep create mode 100644 resources/.gitkeep create mode 100644 resources/PSScriptAnalyzerSettings.psd1.back create mode 100644 resources/TestData/encodingTest.txt create mode 100644 src/Build/Merge-ModuleRoot.Tests.ps1 create mode 100644 src/Build/Merge-ModuleRoot.ps1 create mode 100644 src/Build/New-BuildPackage.Tests.ps1 create mode 100644 src/Build/New-BuildPackage.ps1 create mode 100644 src/Build/Update-ModuleMeta.Tests.ps1 create mode 100644 src/Build/Update-ModuleMeta.ps1 create mode 100644 src/CHANGELOG.md create mode 100644 src/Changelog/Update-Changelog.Tests.ps1 create mode 100644 src/Changelog/Update-Changelog.ps1 create mode 100644 src/Deploy/Invoke-Publish.Tests.ps1 create mode 100644 src/Deploy/Invoke-Publish.ps1 create mode 100644 src/Deps/Install-ModuleDependency.Tests.ps1 create mode 100644 src/Deps/Install-ModuleDependency.ps1 create mode 100644 src/Deps/Invoke-InstallDependency.Tests.ps1 create mode 100644 src/Deps/Invoke-InstallDependency.ps1 create mode 100644 src/Docs/New-Docs.Tests.ps1 create mode 100644 src/Docs/New-Docs.ps1 create mode 100644 src/Docs/Update-Docs.Tests.ps1 create mode 100644 src/Docs/Update-Docs.ps1 create mode 100644 src/DroneHelper.psd1 create mode 100644 src/FileLinter/Invoke-FileLinter.Tests.ps1 create mode 100644 src/FileLinter/Invoke-FileLinter.ps1 create mode 100644 src/FileLinter/Test-FileBOM.Tests.ps1 create mode 100644 src/FileLinter/Test-FileBOM.ps1 create mode 100644 src/FileLinter/Test-FileEOF.Tests.ps1 create mode 100644 src/FileLinter/Test-FileEOF.ps1 create mode 100644 src/FileLinter/Test-FileEOL.Tests.ps1 create mode 100644 src/FileLinter/Test-FileEOL.ps1 create mode 100644 src/FileLinter/Test-FileEncoding.Tests.ps1 create mode 100644 src/FileLinter/Test-FileEncoding.ps1 create mode 100644 src/FileLinter/Test-FileTab.Tests.ps1 create mode 100644 src/FileLinter/Test-FileTab.ps1 create mode 100644 src/FileLinter/Test-FileTailingWhitespace.Tests.ps1 create mode 100644 src/FileLinter/Test-FileTailingWhitespace.ps1 create mode 100644 src/Formats/.gitkeep create mode 100644 src/Helper/Get-RepoPath.Tests.ps1 create mode 100644 src/Helper/Get-RepoPath.ps1 create mode 100644 src/Helper/Set-EOL.Tests.ps1 create mode 100644 src/Helper/Set-EOL.ps1 create mode 100644 src/ModuleRoot.psm1 create mode 100644 src/PRComment/Send-PRComment.Tests.ps1 create mode 100644 src/PRComment/Send-PRComment.ps1 create mode 100644 src/PSModule.Tests.ps1 create mode 100644 src/PSScriptAnalyzer/Invoke-Linter.Tests.ps1 create mode 100644 src/PSScriptAnalyzer/Invoke-Linter.ps1 create mode 100644 src/Pester/Invoke-UnitTest.Tests.ps1 create mode 100644 src/Pester/Invoke-UnitTest.ps1 create mode 100644 src/Reports/Format-FileLinterReport.ps1 create mode 100644 src/Reports/Format-PesterReport.ps1 create mode 100644 src/Reports/Format-ScriptAnalyzerReport.ps1 create mode 100644 src/Rules/PSScriptAnalyzerSettings.psd1 create mode 100644 src/State/Invoke-BuidState.ps1 create mode 100644 src/State/Invoke-BuildState.Tests.ps1 create mode 100644 src/State/Write-FailureStateFile.Tests.ps1 create mode 100644 src/State/Write-FailureStateFile.ps1 create mode 100644 src/State/Write-RestultFile.Tests.ps1 create mode 100644 src/State/Write-ResultFile.ps1 diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..54c292a --- /dev/null +++ b/.drone.yml @@ -0,0 +1,122 @@ +--- +kind: pipeline +type: docker +name: "Build Pipeline" + +trigger: + branch: + exclude: + - droneDocs/* + +steps: + - name: "Pwsh FileLinter" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + failure: ignore + environment: + EXCLUDE: "(.exe|.dll|.ico|.gitkeep)" + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module './src/DroneHelper.psd1'; + Invoke-FileLinter + }" + + - name: "ScriptAnalyzer" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + failure: ignore + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module './src/DroneHelper.psd1'; + Invoke-Linter + }" + + - name: "Pester" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + failure: ignore + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module './src/DroneHelper.psd1'; + Invoke-UnitTest -ExcludeTag 'Integration' + }" + + - name: "PRComment" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + failure: ignore + environment: + GITEA_TOKEN: + from_secret: GITEA_TOKEN + CUSTOM_PIPELINE_STATE: true + LOG_FILES: "build/*.log" + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module './src/DroneHelper.psd1'; + Send-PRComment + }" + when: + event: + include: + - pull_request + + - name: "buildState" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module './src/DroneHelper.psd1'; + Invoke-BuildState + }" + +--- +kind: pipeline +type: docker +name: "Publish Pipeline" + +depends_on: + - "Build Pipeline" + +trigger: + event: + - tag + +steps: + - name: BuildArtifacts + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + #failure: ignore + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module './src/DroneHelper.psd1'; + Update-ModuleMeta -Verbose + }" + - | + pwsh -NonInteractive -c "& { + Import-Module './src/DroneHelper.psd1'; + New-BuildPackage -AdditionalPath './src/Rules' -Verbose + }" + + - name: GiteaRelease + image: plugins/gitea-release + settings: + api_key: + from_secret: GITEA_TOKEN + base_url: https://gitea.ocram85.com + files: + - "bin/${DRONE_REPO_NAME}.zip" + title: ${DRONE_TAG} + note: CHANGELOG.md + + - name: "PublishModule" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + #failure: ignore + environment: + NexusToken: + from_secret: NexusToken + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module './src/DroneHelper.psd1'; + Invoke-Publish -Verbose + }" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d4ef14e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + + +[*.yml] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7d93203 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,21 @@ +# images + +*.jpg binary +*.jpeg binary +*.bmp binary +*.tiff binary +*.png binary +*.svg binary +*.ico binary + +# binary files +*.exe binary +*.dll binary + +# PowerShell specific +*.ps1 working-tree-encoding=UTF-8 +*.psm1 working-tree-encoding=UTF-8 +*.psd1 working-tree-encoding=UTF-8 + +# set markdown to lf for online editing +*.md working-tree-encoding=UTF-8 diff --git a/.gitea/ISSUE_TEMPLATE.md b/.gitea/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..5109ea2 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE.md @@ -0,0 +1,11 @@ +--- +name: "Default" +about: "Use this template if nothing seems to work." +title: ":question: " +labels: + - question +--- + +#### :grey_question: Simply ask your question here: :grey_question: + + diff --git a/.gitea/ISSUE_TEMPLATE/Bug.md b/.gitea/ISSUE_TEMPLATE/Bug.md new file mode 100644 index 0000000..2253eeb --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/Bug.md @@ -0,0 +1,48 @@ +--- +name: "Bug" +about: "This template is used to report bugs!" +title: ":lady_beetle: " +labels: + - bug +--- + + +#### :bomb: Steps to reproduce + +```bash + +``` + +#### :rocket: Expected behavior + +```bash + +``` + +#### :boom: Actual behavior + +```bash + +``` + +#### :notebook: Environment data + + + +```bash + +``` + +#### :framed_picture: Screenshots + + + +#### :bookmark: Refs + + diff --git a/.gitea/ISSUE_TEMPLATE/Enhancement.md b/.gitea/ISSUE_TEMPLATE/Enhancement.md new file mode 100644 index 0000000..ddd4bd5 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/Enhancement.md @@ -0,0 +1,24 @@ +--- +name: "Enhancement" +about: "Wite about new features." +title: ":flying_saucer: " +labels: + - enhancement +--- + + +#### :satellite: Suggestion + + + +#### :artificial_satellite: Implementation ideas + + + +#### :framed_picture: Mock-up Images + + + +#### :bookmark: Refs. diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..02b8ef6 --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +#### :book: Summary + + + +#### :bookmark_tabs: Test Plan + +> :bulb: Select your test plan for the code changes. + +- [ ] Tested via Drone.io pipeline +- [ ] Custom test +- [ ] No test plan + +##### Details / Justification + + + +#### :books: Additional Notes + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4d9d04 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Basic ignore patterns +*.log +*.secret + +# data dir related prod files +data/*.csv + +# Ignore templ build artifacts +/bin/* +!bin/.gitkeep + +# Ignore Unit Test result files +coverage.xml +testResults.xml diff --git a/.gitlocal b/.gitlocal new file mode 100644 index 0000000..4783557 --- /dev/null +++ b/.gitlocal @@ -0,0 +1,19 @@ +[alias] + +# simplified logs +log1 = log --graph --abbrev-commit --decorate --date=relative --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all +log2 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n'' %C(white)%s%C(reset) %C(dim white)- %an%C(reset)' --all + +# Phabricator inspired workflow +# 1. Create new feature branch +feature = "!f(){ b=$1; git checkout master; git pull; git checkout -b "$b" master; };f" + +# 2. Working with code +wip = !"git add -A; git commit -m '[WIP]'" +squish = !"git add -A; git commit --no-edit --amend" + +# 3. Push to origin +pod = !"git push origin dev" + +# 4. Push to custom remote branch +poc = "!f(){ b=$1; git push origin "$b";};f" \ No newline at end of file diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json new file mode 100644 index 0000000..bc784a4 --- /dev/null +++ b/.vscode/cSpell.json @@ -0,0 +1,22 @@ +// cSpell Settings +{ + // Version of the setting file. Always 0.1 + "version": "0.1", + // language - current active spelling language + "language": "en,de,de-DE", + // words - list of words to be always considered correct + "words": [], + // flagWords - list of words to be always considered incorrect + // This is useful for offensive words and common spelling errors. + // For example "hte" should be "the" + "flagWords": [], + "dictionaryDefinitions": [ + { + "name": "default", + "path": "./dictionaries/default.txt" + } + ], + "dictionaries": [ + "default" + ] +} diff --git a/.vscode/dictionaries/default.txt b/.vscode/dictionaries/default.txt new file mode 100644 index 0000000..e69de29 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..e4dfb7c --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "streetsidesoftware.code-spell-checker", + "streetsidesoftware.code-spell-checker-german", + "editorconfig.editorconfig", + "eamodio.gitlens", + "vscode-icons-team.vscode-icons", + "ryanluker.vscode-coverage-gutters" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..de0c692 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,48 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File", + "script": "${file}", + "args": [], + "cwd": "${file}" + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File in Temporary Console", + "script": "${file}", + "args": [], + "cwd": "${file}", + "createTemporaryIntegratedConsole": true + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File w/Args Prompt", + "script": "${file}", + "args": [ + "${command:SpecifyScriptArgs}" + ], + "cwd": "${file}" + }, + { + "type": "PowerShell", + "request": "attach", + "name": "PowerShell Attach to Host Process", + "processId": "${command:PickPSHostProcess}", + "runspaceId": 1 + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Interactive Session", + "cwd": "${workspaceRoot}" + } + ] +} diff --git a/.vscode/pwsh.code-snippets b/.vscode/pwsh.code-snippets new file mode 100644 index 0000000..e3c06b9 --- /dev/null +++ b/.vscode/pwsh.code-snippets @@ -0,0 +1,31 @@ +{ + // Place your PowerShell-Module workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + // PSScriptAnalyzder Rule Suppression + "Rule Suppression": { + "scope": "powershell", + "prefix": "[Diag", + "description": "Suppresses Scriptanalyzer Rules", + "body": [ + "[Diagnostics.CodeAnalysis.SuppressMessageAttribute(", + " '${1|PSProvideCommentHelp,PSAvoidLongLines,PSAvoidUsingWriteHost,PSUseShouldProcessForStateChangingFunctions,PSUseConsistentWhitespace|}',", + " '',", + " Justification = '${justification}'", + ")]" + ] + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1c84e8f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,62 @@ +{ + "files.encoding": "utf8", + "files.eol": "auto", + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "editor.renderWhitespace": "boundary", + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.rulers": [ + 116 + ], + "cSpell.enabled": true, + "cSpell.enabledLanguageIds": [ + "c", + "cpp", + "csharp", + "go", + "javascript", + "javascriptreact", + "json", + "latex", + "markdown", + "php", + "plaintext", + "powershell", + "python", + "text", + "typescript", + "typescriptreact", + "yaml", + "yml" + ], + "cSpell.language": "en,de,de-DE", + // powershell general + "powershell.startAutomatically": true, + "powershell.useX86Host": false, + "powershell.enableProfileLoading": true, + "powershell.scriptAnalysis.enable": true, + // powershell code Formatting + "powershell.codeFormatting.openBraceOnSameLine": true, + "powershell.codeFormatting.newLineAfterOpenBrace": true, + "powershell.codeFormatting.newLineAfterCloseBrace": true, + "powershell.codeFormatting.whitespaceBeforeOpenBrace": true, + "powershell.codeFormatting.whitespaceBeforeOpenParen": true, + "powershell.codeFormatting.whitespaceAroundOperator": true, + "powershell.codeFormatting.whitespaceAfterSeparator": true, + "powershell.codeFormatting.ignoreOneLineBlock": true, + "powershell.codeFormatting.alignPropertyValuePairs": true, + "powershell.scriptAnalysis.settingsPath": "resources/PSScriptAnalyzerSettings.psd1", + "[yaml]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.quickSuggestions": { + "other": true, + "comments": false, + "strings": true + }, + "editor.autoIndent": "full" + }, + "coverage-gutters.showGutterCoverage": false, + "coverage-gutters.showLineCoverage": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..450de84 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,196 @@ +// A task runner that invokes Pester to run all Pester tests under the +// current workspace folder. +// NOTE: This Test task runner requires an updated version of Pester (>=4.0.3) +// in order for the problemMatcher to find failed test information (message, line, file). +// If you don't have that version, you can update Pester from the PowerShell Gallery +// with this command: +// +// PS C:\> Update-Module Pester +// +// If that gives an error like: +// "Module 'Pester' was not installed by using Install-Module, so it cannot be updated." +// then execute: +// +// PS C:\> Install-Module Pester -Scope CurrentUser -Force +// +// NOTE: The Clean, Build and Publish tasks require PSake. PSake can be installed +// from the PowerShell Gallery with this command: +// +// PS C:\> Install-Module PSake -Scope CurrentUser -Force +// +// Available variables which can be used inside of strings: +// ${workspaceFolder} the path of the workspace folder that contains the tasks.json file +// ${workspaceFolderBasename} the name of the workspace folder that contains the tasks.json file without any slashes (/) +// ${file} the current opened file +// ${relativeFile} the current opened file relative to the workspace folder containing the file +// ${fileBasename} the current opened file's basename +// ${fileBasenameNoExtension} the current opened file's basename without the extension +// ${fileDirname} the current opened file's dirname +// ${fileExtname} the current opened file's extension +// ${cwd} the task runner's current working directory on startup +// ${lineNumber} the current selected line number in the active file +{ + "version": "2.0.0", + //"windows": { + // "options": { + // "shell": { + // // switch back to windows powershell 5.1 + // // "executable": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + // "executable": "pwsh.exe", + // "args": [ + // "-NoProfile", + // "-ExecutionPolicy", + // "Bypass", + // "-Command" + // ] + // } + // } + //}, + //"linux": { + // "options": { + // "shell": { + // "executable": "/usr/bin/pwsh", + // "args": [ + // "-NoProfile", + // "-Command" + // ] + // } + // } + //}, + //"osx": { + // "options": { + // "shell": { + // "executable": "/usr/local/bin/pwsh", + // "args": [ + // "-NoProfile", + // "-Command" + // ] + // } + // } + //}, + "tasks": [ + { + "label": "DroneHelper: Invoke-FileLinter", + "type": "shell", + "command": [ + "Remove-Item ./build/FileLinter-Results.log -ErrorAction 'SilentlyContinue';", + "Remove-Item ./failure.log -ErrorAction 'SilentlyContinue';", + "Import-Module ./src/DroneHelper.psd1;", + "Invoke-FileLinter" + ], + "group": "test", + "problemMatcher": [ + "$pester" + ] + }, + { + "label": "DroneHelper: Invoke-Linter", + "type": "shell", + "command": [ + "Remove-Item ./build/ScriptAnalyzer-Results.log -ErrorAction 'SilentlyContinue';", + "Remove-Item ./failure.log -ErrorAction 'SilentlyContinue';", + "Import-Module ./src/DroneHelper.psd1;", + "Invoke-Linter" + ], + "group": "test", + "problemMatcher": [ + "$pester" + ] + }, + { + "label": "DroneHelper: Invoke-UnitTest", + "type": "shell", + "command": [ + "Remove-Item ./coverage.xml -ErrorAction 'SilentlyContinue';", + "Remove-Item ./testResults.xml -ErrorAction 'SilentlyContinue';", + "Remove-Item ./build/FileLinter-Results.log -ErrorAction 'SilentlyContinue';", + "Remove-Item ./build/Pester-Results.log -ErrorAction 'SilentlyContinue';", + "Remove-Item ./failure.log -ErrorAction 'SilentlyContinue';", + "Import-Module ./src/DroneHelper.psd1;", + "Invoke-UnitTest -CoverageFormat 'CoverageGutters' -Verbosity 'Detailed'" + ], + "group": "test", + "problemMatcher": [ + "$pester" + ] + }, + { + "label": "DroneHelper: Full Test", + "type": "shell", + "command": [ + "Remove-Item ./build/ScriptAnalyzer-Results.log -ErrorAction 'SilentlyContinue';", + "Remove-Item ./build/FileLinter-Results.log -ErrorAction 'SilentlyContinue';", + "Remove-Item ./build/Pester-Results.log -ErrorAction 'SilentlyContinue';", + "Remove-Item ./coverage.xml -ErrorAction 'SilentlyContinue';", + "Remove-Item ./testResults.xml -ErrorAction 'SilentlyContinue';", + "Remove-Item ./failure.log -ErrorAction 'SilentlyContinue';", + "Import-Module ./src/DroneHelper.psd1;", + "Invoke-FileLinter;", + "Invoke-Linter;", + "Invoke-UnitTest -CoverageFormat 'CoverageGutters' -Verbosity 'Detailed'" + ], + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [ + "$pester" + ] + }, + { + "label": "Add dictionary item", + "type": "shell", + "command": [ + "$DefaultFile = Get-ChildItem -Path './.vscode/dictionaries/default.txt';", + "$Content = Get-Content -Path $DefaultFile;", + "$Content += '${input:DictionaryItem}';", + "$Content = $Content | Sort-Object -Unique;", + "Set-Content -Value $Content -Path $DefaultFile" + ], + "group": "none", + "problemMatcher": [ + "$pester" + ] + }, + { + "label": "DroneHelper: New Docs", + "type": "shell", + "command": [ + "Import-Module ./src/DroneHelper.psd1;", + "New-Docs" + ], + "problemMatcher": [] + }, + { + "label": "DroneHelper: Update Docs", + "type": "shell", + "command": [ + "Import-Module ./src/DroneHelper.psd1;", + "Update-Docs" + ], + "problemMatcher": [] + }, + { + "label": "DroneHelper: Update Changelog", + "type": "shell", + "command": [ + "Import-Module ./src/DroneHelper.psd1;", + "Update-Changelog -NewVersion '${input:NewVersion}'" + ], + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "DictionaryItem", + "type": "promptString", + "description": "Input for dictionary file default.txt" + }, + { + "id": "NewVersion", + "type": "promptString", + "description": "Enter a new semver version id", + "default": "v1.0.0" + } + ] +} diff --git a/README.md b/README.md index 5a7da58..d30f772 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,194 @@ # DroneHelper -Helper module for Drone.io CI. \ No newline at end of file +[![Build Status](https://drone.ocram85.com/api/badges/OCram85/DroneHelper/status.svg)](https://drone.ocram85.com/OCram85/DroneHelper) + +## Description + +Helper module for Drone.io based build pipelines. + +## About + +The DroneHelper PowerShell Modules adds several feature to a `Gitea -> Drone.IO` based build pipeline. It's designed +to perform all needed tasks for PowerShell Module development like: + +- `FileLinter` -> Runs basic FileLinter tests with console and log file output +- `Linter` -> Runs PSScriptAnalyer with embedded to custom profiles. +- `UnitTest` -> Executes Pester tests with code coverage with console and log file output. +- `BuildReport` -> Takes all generated reports and reports them back as Pull Request Comment for a simplified overview. +- `StateReporter` -> Marks the current pipeline run / build as failed if the previous steps also raised errors. +- `DoksUpdater` -> Automatically updates the markdown based docs generated form your Comment Based Help blocks in your functions + +To use these feature, all you have to do, is follow the `.drone.yml` template: + +### `.drone.yml` Template + +```yaml +--- +kind: pipeline +type: docker +name: "Build Pipeline" + +trigger: + branch: + exclude: + - droneDocs/* + +steps: + - name: "Pwsh FileLinter" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + failure: ignore + environment: + EXCLUDE: "(.exe|.dll|.ico|.gitkeep)" + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module 'DroneHelper'; + Invoke-FileLinter + }" + + - name: "ScriptAnalyzer" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + failure: ignore + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module -Name 'DroneHelper'; + Install-ModuleDependency; + Invoke-Linter + }" + + - name: "Pester" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + failure: ignore + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module -Name 'DroneHelper'; + Install-ModuleDependency; + Invoke-UnitTest -Verbosity 'Detailed' -ExcludeTag 'Integration' + }" + + - name: "PRComment" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + failure: ignore + environment: + GITEA_TOKEN: + from_secret: GITEA_TOKEN + CUSTOM_PIPELINE_STATE: true + LOG_FILES: "build/*.log" + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module 'DroneHelper'; + Send-PRComment + }" + when: + event: + include: + - pull_request + + - name: "buildState" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module -Name 'DroneHelper'; + Install-ModuleDependency; + Invoke-BuildState + }" + +--- +kind: pipeline +type: docker +name: "Publish Pipeline" + +depends_on: + - "Build Pipeline" + +trigger: + event: + - tag + +steps: + - name: BuildArtifacts + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + #failure: ignore + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module -Name 'DroneHelper'; + Install-ModuleDependency; + Update-ModuleMeta -Verbose + }" + - | + pwsh -NonInteractive -c "& { + Import-Module -Name 'DroneHelper'; + Install-ModuleDependency; + New-BuildPackage -Verbose + }" + + - name: GiteaRelease + image: plugins/gitea-release + settings: + api_key: + from_secret: GITEA_TOKEN + base_url: https://gitea.ocram85.com + files: + - "bin/${DRONE_REPO_NAME}.zip" + - "bin/PSModule.zip" + title: "${DRONE_TAG}" + note: CHANGELOG.md + + - name: "PublishModule" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + #failure: ignore + environment: + NexusToken: + from_secret: NexusToken + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module -Name 'DroneHelper'; + Install-ModuleDependency; + Invoke-Publish -Verbose + }" + +--- +kind: pipeline +type: docker +name: "Update Docs" + +depends_on: + - "Build Pipeline" + +trigger: + event: + exclude: + - pull_request + branch: + include: + - master + +steps: + - name: "Update Docs" + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + commands: + - | + pwsh -NonInteractive -c "& { + Import-Module -Name 'DroneHelper'; + Install-ModuleDependency; + Update-Docs -Verbose + }" + + - name: "push commit" + image: appleboy/drone-git-push + settings: + remote_name: origin + branch: "droneDocs/${DRONE_COMMIT:0:8}" + local_ref: droneDocs + commit: true + commit_message: "docs updated by drone [CI SKIP]" + author_name: drone + author_email: noreply@ocram85.com + force: true +``` diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 0000000..98ea7c2 --- /dev/null +++ b/assets/.gitkeep @@ -0,0 +1 @@ +git placeholder file diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..98ea7c2 --- /dev/null +++ b/bin/.gitkeep @@ -0,0 +1 @@ +git placeholder file diff --git a/build/.gitkeep b/build/.gitkeep new file mode 100644 index 0000000..98ea7c2 --- /dev/null +++ b/build/.gitkeep @@ -0,0 +1 @@ +git placeholder file diff --git a/resources/.gitkeep b/resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/PSScriptAnalyzerSettings.psd1.back b/resources/PSScriptAnalyzerSettings.psd1.back new file mode 100644 index 0000000..8ba8ac8 --- /dev/null +++ b/resources/PSScriptAnalyzerSettings.psd1.back @@ -0,0 +1,141 @@ +@{ + Severity = 'Error', 'Warning', 'Information' + + IncludeRules = @( + 'PSAlignAssignmentStatement', + 'PSAvoidAssignmentToAutomaticVariable', + 'PSAvoidDefaultValueForMandatoryParameter', + 'PSAvoidDefaultValueSwitchParameter', + 'PSAvoidGlobalAliases', + 'PSAvoidGlobalFunctions', + 'PSAvoidGlobalVars', + 'PSAvoidInvokingEmptyMembers', + 'PSAvoidLongLines', + 'PSAvoidNullOrEmptyHelpMessageAttribute', + 'PSAvoidOverwritingBuiltInCmdlets', + 'PSAvoidShouldContinueWithoutForce', + 'PSAvoidTrailingWhitespace', + 'PSAvoidUsingCmdletAliases', + 'PSAvoidUsingComputerNameHardcoded', + 'PSAvoidUsingConvertToSecureStringWithPlainText', + 'PSAvoidUsingDeprecatedManifestFields', + 'PSAvoidUsingDoubleQuotesForConstantString', + 'PSAvoidUsingEmptyCatchBlock', + 'PSAvoidUsingInvokeExpression', + 'PSAvoidUsingPlainTextForPassword', + 'PSAvoidUsingPositionalParameters', + 'PSAvoidUsingUsernameAndPasswordParams', + 'PSAvoidUsingWMICmdlet', + 'PSAvoidUsingWriteHost', + 'PSMisleadingBacktick', + 'PSMissingModuleManifestField', + 'PSPlaceCloseBrace', + 'PSPlaceOpenBrace', + 'PSPossibleIncorrectComparisonWithNull', + 'PSPossibleIncorrectUsageOfAssignmentOperator', + 'PSPossibleIncorrectUsageOfRedirectionOperator', + 'PSProvideCommentHelp', + 'PSReservedCmdletChar', + 'PSReservedParams', + 'PSReviewUnusedParameter', + 'PSShouldProcess', + 'PSUseApprovedVerbs', + 'PSUseBOMForUnicodeEncodedFile', + 'PSUseCmdletCorrectly', + # There is no predefined set for Pwsh7 Cmdlets + 'PSUseCompatibleCmdlets', + #'PSUseCompatibleCommands', + 'PSUseCompatibleSyntax', + #'PSUseCompatibleTypes', + 'PSUseConsistentIndentation', + # Disable if bug in 1.19.1 version occurs. + 'PSUseConsistentWhitespace', + 'PSUseCorrectCasing', + 'PSUseDeclaredVarsMoreThanAssignments', + 'PSUseLiteralInitializerForHashtable', + 'PSUseOutputTypeCorrectly', + 'PSUsePSCredentialType', + 'PSUseProcessBlockForPipelineCommand', + 'PSUseShouldProcessForStateChangingFunctions', + 'PSUseSingularNouns', + 'PSUseSupportsShouldProcess', + 'PSUseToExportFieldsInManifest', + 'PSUseUTF8EncodingForHelpFile', + 'PSUseUsingScopeModifierInNewRunspaces' + ) + + Rules = @{ + + PSAvoidLongLines = @{ + Enable = $true + MaximumLineLength = 116 + } + + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + + PSPlaceCloseBrace = @{ + Enable = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + NoEmptyLineBefore = $false + } + + PSProvideCommentHelp = @{ + Enable = $true + ExportedOnly = $false + BlockComment = $true + VSCodeSnippetCorrection = $false + Placement = "begin" + } + + PSUseCompatibleCmdlets = @{ + compatibility = @( + "desktop-5.1.14393.206-windows", + "core-6.1.0-windows" + ) + } + + PSUseCompatibleSyntax = @{ + Enable = $true + TargetVersions = @( + "7.0", + "5.1" + ) + } + + PSUseConsistentIndentation = @{ + Enable = $true + Kind = 'space' + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IndentationSize = 4 + } + + PSUseConsistentWhitespace = @{ + Enable = $true + CheckInnerBrace = $true + CheckOpenBrace = $true + CheckOpenParen = $true + CheckOperator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $false + CheckSeparator = $true + CheckParameter = $false + IgnoreAssignmentOperatorInsideHashTable = $true + } + + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $false + } + + PSUseCorrectCasing = @{ + Enable = $true + } + } + +} diff --git a/resources/TestData/encodingTest.txt b/resources/TestData/encodingTest.txt new file mode 100644 index 0000000..2f74a17 --- /dev/null +++ b/resources/TestData/encodingTest.txt @@ -0,0 +1,9 @@ +This is just a test file. Used for testing the encoding linter + +#### Test Data #### + +ABCD +abcdefg +12345678 +öäü +!"§$%&/() diff --git a/src/Build/Merge-ModuleRoot.Tests.ps1 b/src/Build/Merge-ModuleRoot.Tests.ps1 new file mode 100644 index 0000000..3cd3215 --- /dev/null +++ b/src/Build/Merge-ModuleRoot.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Merge-ModuleRoot' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Merge-ModuleRoot' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Merge-ModuleRoot' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Merge-ModuleRoot' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/Build/Merge-ModuleRoot.ps1 b/src/Build/Merge-ModuleRoot.ps1 new file mode 100644 index 0000000..d75c3cf --- /dev/null +++ b/src/Build/Merge-ModuleRoot.ps1 @@ -0,0 +1,42 @@ +function Merge-ModuleRoot { + <# + .SYNOPSIS + Merges single ps1 files into one module script file. + + .DESCRIPTION + This Cmdlet is used in build pipeline to reduce the file load and import performance to the target module. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] No pipeline output. + + .EXAMPLE + Import-Module -Name DroneHelper; Merge-ModuleRoot + #> + [CmdletBinding()] + param () + + process { + $Repo = Get-RepoPath + + $srcFiles = Get-ChildItem -Path $Repo.Src.Path -Recurse -File | Where-Object { + ($_.Name -notmatch '.Tests.') -and ($_.Name -match '.ps1') -and ($_.Name -notmatch '.ps1xml') + } + $Output = @() + foreach ($psFile in $srcFiles) { + $fileContent = Get-Content -Path $psFile.FullName -Raw -Encoding 'utf8' + $Output += '# srcFile: {0}' -f $psFile.FullName + $Output += $fileContent.TrimEnd() + $Output += '{0}' -f [Environment]::NewLine + } + try { + $Output | Out-File -FilePath $Repo.Bin.ScriptModuleName -Encoding 'utf8' -Force -ErrorAction Stop + } + catch { + Write-FailureStateFile -StepName 'MergeModuleRoot' + throw 'Could not write the final module root script file!' + } + } +} diff --git a/src/Build/New-BuildPackage.Tests.ps1 b/src/Build/New-BuildPackage.Tests.ps1 new file mode 100644 index 0000000..9014158 --- /dev/null +++ b/src/Build/New-BuildPackage.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'New-BuildPackage' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'New-BuildPackage' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'New-BuildPackage' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'New-BuildPackage' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/Build/New-BuildPackage.ps1 b/src/Build/New-BuildPackage.ps1 new file mode 100644 index 0000000..5985a2a --- /dev/null +++ b/src/Build/New-BuildPackage.ps1 @@ -0,0 +1,74 @@ +function New-BuildPackage { + <# + .SYNOPSIS + Creates a new module package as compressed archive. + + .DESCRIPTION + This function is used in build pipeline to create an uploadable module version for the Gitea release page. + + .PARAMETER AdditionalPath + You can provide additional paths to add files or folders in published module. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] No pipeline output. + + .EXAMPLE + Import-Module -Name DroneHelper; New-BuildPackage + #> + [CmdletBinding()] + [OutputType()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseConsistentWhitespace', + '', + Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'system state does not change permanent in temp build clients.' + )] + + param ( + [Parameter(Mandatory = $false)] + [String[]]$AdditionalPath + ) + + process { + $Repo = Get-RepoPath + $res = @() + foreach ($item in $AdditionalPath) { + try { + $res += Resolve-Path -Path $item -ErrorAction Stop + } + catch { + Write-Error -Message ('The given additional path does not exist! ({0})' -f $item) -ErrorAction Stop + } + } + Merge-ModuleRoot -ErrorAction Stop + $CompressParams = @{ + Path = @( + # psm1 file + $Repo.Bin.ScriptModuleName + # psd1 file + $Repo.Src.Manifest.Item.FullName + # Formats/ folder + $Repo.Src.Formats.Path + ) + DestinationPath = $Repo.Bin.ArtifactPath + Force = $true + ErrorAction = 'Stop' + Verbose = $VerbosePreference + } + $CompressParams.Path += $res + try { + Compress-Archive @CompressParams + } + catch { + Write-FailureStateFile -StepName 'BuildPackage' + throw $_.Exception.Message + } + } +} diff --git a/src/Build/Update-ModuleMeta.Tests.ps1 b/src/Build/Update-ModuleMeta.Tests.ps1 new file mode 100644 index 0000000..9140089 --- /dev/null +++ b/src/Build/Update-ModuleMeta.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Update-ModuleMeta' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Update-ModuleMeta' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Update-ModuleMeta' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Update-ModuleMeta' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/Build/Update-ModuleMeta.ps1 b/src/Build/Update-ModuleMeta.ps1 new file mode 100644 index 0000000..124f342 --- /dev/null +++ b/src/Build/Update-ModuleMeta.ps1 @@ -0,0 +1,75 @@ +function Update-ModuleMeta { + <# + .SYNOPSIS + Updates the module manifest file fields to prepare the new build. + + .DESCRIPTION + Replaces the version fields in the manifest file. Uses Drone env vars populated by pushed tags. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] No pipeline output. + + .EXAMPLE + Import-Module -Name DroneHelper; Update-ModuleMeta + #> + [CmdletBinding()] + [OutputType()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseConsistentWhitespace', + '', + Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'system state does not change permanent in temp build clients.' + )] + param () + + process { + if ($Env:DRONE) { + $Repo = Get-RepoPath + if ($Env:DRONE_BUILD_EVENT -eq 'tag') { + if ($null -ne $Env:DRONE_SEMVER) { + $nVersion = $Env:DRONE_SEMVER_SHORT + + if ($null -ne $Env:DRONE_SEMVER_PRERELEASE) { + $nPreRelease = $Env:DRONE_SEMVER_PRERELEASE + } + $ModManifestParams = @{ + Path = $Repo.Src.Manifest.Item.FullName + ModuleVersion = $nVersion + ErrorAction = 'Stop' + } + if ($nPreRelease) { + $ModManifestParams.PreRelease = $nPreRelease + } + + $ManifestData = Test-ModuleManifest -Path $Repo.Src.Manifest.Item.FullName + + if ( + ($nVersion -ne $ManifestData.Version) -or + ($nVersion -ne $ManifestData.PrivateData.PSData.Prerelease) + ) { + Update-ModuleManifest @ModManifestParams + } + else { + Write-Verbose -Message 'Identical version given. Skipping update.' + } + } + else { + Write-Verbose -Message 'Could not read the new Tag / Semver!' + } + } + else { + Write-Verbose -Message 'This pipeline was not triggered by a tag.' + } + } + else { + Write-Verbose -Message 'Running outside of drone.io pipeline. Skipping module update!' + } + } +} diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md new file mode 100644 index 0000000..942be0d --- /dev/null +++ b/src/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + + diff --git a/src/Changelog/Update-Changelog.Tests.ps1 b/src/Changelog/Update-Changelog.Tests.ps1 new file mode 100644 index 0000000..a858928 --- /dev/null +++ b/src/Changelog/Update-Changelog.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Update-Changelog' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Update-Changelog' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Update-Changelog' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Update-Changelog' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/Changelog/Update-Changelog.ps1 b/src/Changelog/Update-Changelog.ps1 new file mode 100644 index 0000000..517306a --- /dev/null +++ b/src/Changelog/Update-Changelog.ps1 @@ -0,0 +1,93 @@ +function Update-Changelog { + <# + .SYNOPSIS + Updates the changelog file with recent commits + + .DESCRIPTION + This helper function is used to insert recent changes for an upcoming release. + + .Parameter NewVersion + Provide a valid semver based version tag for the upcoming release like: + + - `v0.0.1-dev1` + - `v1.0.0` + + .Parameter SkipCleanup + You can skip the tag update and additional test. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] no pipeline putput. + + .EXAMPLE + Import-Module -Name DroneHelper; Update-Changelog -NewVersion '0.0.1-dev5' + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingInvokeExpression', + '', + Justification = 'raw git commands needed' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'system state does not change permanent in temp build clients.' + )] + param ( + [Parameter(Mandatory = $true)] + [ValidatePattern('^(v)?([0-9]+)\.([0-9]+)\.([0-9]+)(-([A-Za-z]+)([0-9]+))?$')] + [String]$NewVersion, + + [Parameter(Mandatory = $false)] + [Switch]$SkipCleanup + ) + process { + if (-not $SkipCleanup.IsPresent) { + Invoke-Expression -Command 'git tag -d $(git tag -l)' | Write-Verbose + Invoke-Expression -Command 'git fetch --tags --prune' | Write-Verbose + $GitState = Get-GitStatus + + if ($GitState.Branch -eq 'master') { + Write-Error -Message 'You can nor update the changelog within the master branch!' -ErrorAction Stop + } + if ( + ($GitState.BehindBy -ne 0) -or + ($GitState.AheadBy -ne 0) -or + ($GitState.HasUntracked -ne $false) -or + ($GitState.HasWorking -ne $false) + ) { + Write-Error -Message 'Your branch is a mess! Cleanup and try it again.' -ErrorAction Stop + } + } + + $Repo = Get-RepoPath + $Tags = Invoke-Expression -Command 'git tag' + $NormalizedTags = $Tags | Where-Object { $_ -notmatch '-' } | ForEach-Object { + [PSCustomObject]@{ + tag = $_ + rawVersion = ( + ($_ -split '-')[0] -replace 'v', '' + ) + } + } + $LTag = $NormalizedTags | Sort-Object -Property 'rawVersion' | Select-Object -ExpandProperty 'tag' -Last 1 + Write-Debug -Message ('Last tag: {0}' -f $LTag) + if ($null -eq $LTag) { + Write-Error -Message 'No tags found!' -ErrorAction 'Stop' + } + $Expr = "git log {0}..HEAD --format='- (%h) %s'" -f $LTag + $Res = Invoke-Expression -Command $Expr + Write-Debug -Message ('New Changelog: {0}' -f $Res) + if ($Repo.Changelog.Exists) { + $Content = Get-Content -Path $Repo.Changelog.Item.FullName + $Content[2] += "{0}## ``{2}``{0}{0}{1}" -f [Environment]::NewLine, ($Res | Out-String), $NewVersion + $Content | Out-File -FilePath $Repo.Changelog.Item.FullName -Encoding utf8 + Set-EOL -Path $Repo.Changelog.Item.FullName + } + else { + Write-Error -Message 'Changelog file does not exist!' -ErrorAction Stop + } + } +} diff --git a/src/Deploy/Invoke-Publish.Tests.ps1 b/src/Deploy/Invoke-Publish.Tests.ps1 new file mode 100644 index 0000000..d44c7a3 --- /dev/null +++ b/src/Deploy/Invoke-Publish.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Invoke-Publish' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Invoke-Publish' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Invoke-Publish' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Invoke-Publish' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/Deploy/Invoke-Publish.ps1 b/src/Deploy/Invoke-Publish.ps1 new file mode 100644 index 0000000..f57c3c8 --- /dev/null +++ b/src/Deploy/Invoke-Publish.ps1 @@ -0,0 +1,46 @@ +function Invoke-Publish { + <# + .SYNOPSIS + Publishes powershell module to internal Nexus repository. + + .DESCRIPTION + This Cmdlet is used to publish the module via Drone pipeline. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] No pipeline output. + + .EXAMPLE + Invoke-Publish + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseConsistentWhitespace', + '', + Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' + )] + param () + + process { + $Repo = Get-RepoPath + $ExpandParams = @{ + Path = $Repo.Bin.ArtifactPath + DestinationPath = $Repo.Bin.ExpandPath + Force = $true + ErrorAction = 'Stop' + Verbose = $VerbosePreference + } + Expand-Archive @ExpandParams + + $PublishParams = @{ + Repository = 'Nexus' + Path = $Repo.Bin.ExpandPath + NuGetApiKey = $Env:NexusToken + Verbose = $VerbosePreference + ErrorAction = 'Stop' + } + Publish-Module @PublishParams + } +} diff --git a/src/Deps/Install-ModuleDependency.Tests.ps1 b/src/Deps/Install-ModuleDependency.Tests.ps1 new file mode 100644 index 0000000..f3bce26 --- /dev/null +++ b/src/Deps/Install-ModuleDependency.Tests.ps1 @@ -0,0 +1,32 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Install-ModuleDependency' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Install-ModuleDependency' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Install-ModuleDependency' } | Should -Not -Throw + } + + It 'Help Content' { + $foo = Get-Help -Name 'Install-ModuleDependency' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } + Context 'Unit tests' -Tag 'Unit' { + It 'Should throw' { + { Install-ModuleDependency -foo } | Should -Throw + } + + #It 'Should not throw' { + # Mock 'Install-Module' -ModuleName $Repo.Artifact {} + # { Install-ModuleDependency } | Should -Not -Throw + #} + } +} diff --git a/src/Deps/Install-ModuleDependency.ps1 b/src/Deps/Install-ModuleDependency.ps1 new file mode 100644 index 0000000..ea1791a --- /dev/null +++ b/src/Deps/Install-ModuleDependency.ps1 @@ -0,0 +1,74 @@ +function Install-ModuleDependency { + <# + .SYNOPSIS + Install required modules of the module manifest file. + + .DESCRIPTION + Use this cmdlet to install required modules of the module manifest file. + + .INPUTS + [None] + + .OUTPUTS + [None] + + .EXAMPLE + Install-ModuleDependency + + .NOTES + #> + + [CmdletBinding()] + #[OutputType([String])] + param () + + begin { + } + + process { + $Repo = Get-RepoPath + $ManifestContent = Import-PowerShellDataFile -Path $Repo.Src.Manifest.Item.FullName + if ($ManifestContent.RequiredModules) { + foreach ($Module in $ManifestContent.RequiredModules) { + if ($Module.RequiredVersion) { + $ParamsInstallModule = @{ + Name = $Module.ModuleName + Scope = 'AllUsers' + RequiredVersion = $Module.RequiredVersion + Force = $true + AllowClobber = $true + Verbose = $VerbosePreference + ErrorAction = 'Stop' + } + } + else { + $ParamsInstallModule = @{ + Name = $Module.ModuleName + Scope = 'AllUsers' + MinimumVersion = $Module.ModuleVersion + Force = $true + AllowClobber = $true + Verbose = $VerbosePreference + ErrorAction = 'Stop' + } + } + try { + Install-Module @ParamsInstallModule + $Message = 'Module <{0}> successfully installed' -f $Module.ModuleName + Write-Verbose -Message $Message + } + catch { + $Message = 'Module <{0}> could not be installed! ' -f $Module.ModuleName + $Message += $_.Exception.Message + Write-Error -Message $Message -ErrorAction 'Stop' + } + } + } + else { + Write-Verbose -Message 'no required modules found...' + } + } + + end { + } +} diff --git a/src/Deps/Invoke-InstallDependency.Tests.ps1 b/src/Deps/Invoke-InstallDependency.Tests.ps1 new file mode 100644 index 0000000..2272148 --- /dev/null +++ b/src/Deps/Invoke-InstallDependency.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Invoke-InstallDependency' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Invoke-InstallDependency' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Invoke-InstallDependency' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Invoke-InstallDependency' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/Deps/Invoke-InstallDependency.ps1 b/src/Deps/Invoke-InstallDependency.ps1 new file mode 100644 index 0000000..ca685d2 --- /dev/null +++ b/src/Deps/Invoke-InstallDependency.ps1 @@ -0,0 +1,72 @@ +function Invoke-InstallDependency { + <# + .SYNOPSIS + Install required modules for executing the DroneHelper pipeline helpers. + + .DESCRIPTION + This can be used in drone.io docker pipeline if the modules are not integrated in the build image. + + .INPUTS + [None] No Input required. + + .OUTPUTS + [None] No Output + + .EXAMPLE + Import-Module -Name DroneHelper; Invoke-Install-Dependency + #> + [CmdletBinding()] + [OutputType()] + param () + + process { + try { + $PSScriptParams = @{ + Name = 'PSScriptAnalyzer' + Scope = 'CurrentUser' + RequiredVersion = '1.20.0' + Force = $true + SkipPublisherCheck = $true + AllowClobber = $true + Verbose = $VerbosePreference + ErrorAction = 'Stop' + } + Install-Module @PSScriptParams + + $PesterParams = @{ + Name = 'Pester' + Scope = 'CurrentUser' + RequiredVersion = '5.3.1' + Force = $true + SkipPublisherCheck = $true + AllowClobber = $true + Verbose = $VerbosePreference + ErrorAction = 'Stop' + } + Install-Module @PesterParams + + $PoshParams = @{ + Name = 'posh-git' + Scope = 'CurrentUser' + RequiredVersion = '1.0.0' + Force = $true + SkipPublisherCheck = $true + AllowClobber = $true + Verbose = $VerbosePreference + ErrorAction = 'Stop' + } + Install-Module @PoshParams + } + catch { + $ExecParams = @{ + Exception = [System.Exception]::new( + 'Could not install required build dependencies!', + $PSItem.Exception + ) + ErrorAction = 'Stop' + } + Write-Error @ExecParams + } + + } +} diff --git a/src/Docs/New-Docs.Tests.ps1 b/src/Docs/New-Docs.Tests.ps1 new file mode 100644 index 0000000..a14eade --- /dev/null +++ b/src/Docs/New-Docs.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'New-Docs' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'New-Docs' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'New-Docs' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'New-Docs' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/Docs/New-Docs.ps1 b/src/Docs/New-Docs.ps1 new file mode 100644 index 0000000..184d8ab --- /dev/null +++ b/src/Docs/New-Docs.ps1 @@ -0,0 +1,50 @@ +function New-Docs { + <# + .SYNOPSIS + Creates a ne set of markdown based help in the docs folder. + + .DESCRIPTION + This Cmdlet should be used once locally, or after adding new functions. The function `Update-Docs` + can be used via pipeline to keep the docs up to date. + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] No pipeline output. + + .EXAMPLE + New-Docs + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseSingularNouns', + '', + Justification = 'New-Doc already in use by other popular modules.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'system state does not change permanent in temp build clients.' + )] + param () + + process { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Global -Force + Import-Module -Name 'platyPS' + $MarkdownParams = @{ + Module = $Repo.Artifact + OutputFolder = $Repo.Docs.Path + WithModulePage = $true + ModulePagePath = $Repo.Docs.ModulePagePath + Force = $true + } + New-MarkdownHelp @MarkdownParams + + $Docs = Get-Item -Path $Repo.Docs.MarkdownFilter + foreach ($Doc in $Docs) { + Write-Verbose -Message ('Converting {0}' -f $Doc.FullName) + Set-EOL -Path $Doc + } + } +} diff --git a/src/Docs/Update-Docs.Tests.ps1 b/src/Docs/Update-Docs.Tests.ps1 new file mode 100644 index 0000000..8c9f25e --- /dev/null +++ b/src/Docs/Update-Docs.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Update-Docs' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Update-Docs' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Update-Docs' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Update-Docs' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/Docs/Update-Docs.ps1 b/src/Docs/Update-Docs.ps1 new file mode 100644 index 0000000..2e8e4f1 --- /dev/null +++ b/src/Docs/Update-Docs.ps1 @@ -0,0 +1,49 @@ +function Update-Docs { + <# + .SYNOPSIS + Publishes powershell module to internal Nexus repository. + + .DESCRIPTION + This Cmdlet is used to publish the module via Drone pipeline. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] No pipeline output. + + .EXAMPLE + Update-Docs + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'Underlying platyPS can not be mocked.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseSingularNouns', + '', + Justification = 'New-Doc already in use by other popular modules.' + )] + param () + + process { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Global -Force + Import-Module -Name 'platyPS' + $MarkdownParams = @{ + Path = $Repo.Docs.Path + RefreshModulePage = $true + ModulePagePath = $Repo.Docs.ModulePagePath + Force = $true + } + Update-MarkdownHelpModule @MarkdownParams + + $Docs = Get-Item -Path $Repo.Docs.MarkdownFilter + foreach ($Doc in $Docs) { + Write-Verbose -Message ('Converting {0}' -f $Doc.FullName) + Set-EOL -Path $Doc + } + } +} diff --git a/src/DroneHelper.psd1 b/src/DroneHelper.psd1 new file mode 100644 index 0000000..a0d08af --- /dev/null +++ b/src/DroneHelper.psd1 @@ -0,0 +1,190 @@ +# +# Module manifest for module 'DroneHelper' +# +# Generated by: OCram85 +# +# Generated on: 13.06.2022 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'ModuleRoot.psm1' + + # Version number of this module. + ModuleVersion = '0.0.1' + + # Supported PSEditions + CompatiblePSEditions = @( + 'Desktop', + 'Core' + ) + + # ID used to uniquely identify this module + GUID = '4293292f-eac8-42ed-8e9d-437d4f405d2c' + + # Author of this module + Author = 'OCram85' + + # Company or vendor of this module + CompanyName = '' + + # Copyright statement for this module + Copyright = '(c) OCram85. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Helper module for Drone.io CI.' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '5.1' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. + # This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. + # This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @( + @{ + ModuleName = 'PSScriptAnalyzer' + RequiredVersion = '1.20.0' + }, + @{ + ModuleName = 'Pester' + RequiredVersion = '5.3.1' + }, + @{ + ModuleName = 'posh-git' + ModuleVersion = '1.1.0' + } + ) + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, + # use an empty array if there are no functions to export. + FunctionsToExport = @( + # Build + 'Merge-ModuleRoot', + 'New-BuildPackage', + 'Update-ModuleMeta', + # Changelog + 'Update-Changelog', + # Deploy + 'Invoke-Publish', + # Deps + 'Install-ModuleDependency', + 'Invoke-InstallDependency', + # Docs + 'New-Docs', + 'Update-Docs', + # FileLinter + 'Invoke-FileLinter', + 'Test-FileBOM', + 'Test-FileEncoding', + 'Test-FileEOF', + 'Test-FileEOL', + 'Test-FileTab', + 'Test-FileTailingWhitespace', + # Helper + 'Get-RepoPath', + 'Set-EOL', + # Pester + 'Invoke-UnitTest', + # PRComment + 'Send-PRComment', + # PSScriptAnalyzer + 'Invoke-Linter', + # State + 'Invoke-BuildState', + 'Write-FailureStateFile', + 'Write-ResultFile' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, + # use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, + # use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData + # hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + ProjectUri = 'https://gitea.ocram85.com/OCram85/DroneHelper' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using + # Import-Module -Prefix. + # DefaultCommandPrefix = '' + +} diff --git a/src/FileLinter/Invoke-FileLinter.Tests.ps1 b/src/FileLinter/Invoke-FileLinter.Tests.ps1 new file mode 100644 index 0000000..3da840a --- /dev/null +++ b/src/FileLinter/Invoke-FileLinter.Tests.ps1 @@ -0,0 +1,81 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force + $MoveBackup = @{ + Path = $Repo.Build.FileLinterLogPath + Destination = Join-Path -Path $Repo.Build.Path -ChildPath 'FileLinter.backup' + } + Move-Item @MoveBackup +} + +Describe 'Invoke-FileLinter' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Invoke-FileLinter' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Invoke-FileLinter' } | Should -Not -Throw + } + + It 'Help Content' { + $foo = Get-Help -Name 'Invoke-FileLinter' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } + Context 'Coding tests' -Tag 'Unit' { + # Mocking doesn't get executed. Switch to Integration test class to avoid duplicated output. + BeforeEach { + Mock 'Write-FailureStateFile' { + Write-Debug 'Mocking function Write-FailureStateFile.' + } -ModuleName 'DroneHelper' + + Mock 'Write-ResultFile' { + Write-Debug 'Mocking function Write-ResultFile.' + } -ModuleName 'DroneHelper' + } + It 'Should throw' { + #Mock 'Write-FailureStateFile' { Write-Debug 'Mocking Report generator' } + #Mock 'Write-ResultFile' { Write-Debug 'Mocking function Write-ResultFile.' } + { Invoke-FileLinter -foo } | Should -Throw + } + It 'Should not throw' { + #Mock 'Write-FailureStateFile' { Write-Debug 'Mocking Report generator' } + #Mock 'Write-ResultFile' { Write-Debug 'Mocking function Write-ResultFile.' } + { Invoke-FileLinter } | Should -Not -Throw + } + + It 'Should have found files' { + #Mock Write-FailureStateFile { Write-Debug 'Mocking Report generator' } + #Mock 'Write-ResultFile' { Write-Debug 'Mocking function Write-ResultFile.' } + $res = Invoke-FileLinter -PassThru + $res.FilesCount | Should -Not -BeNullOrEmpty + $res.FilesCount | Should -BeGreaterThan 0 + } + + It 'Should run without linter issues' { + #Mock 'Write-FailureStateFile' { Write-Debug 'Mocking Report generator' } + #Mock 'Write-ResultFile' { Write-Debug 'Mocking function Write-ResultFile.' } + $res = Invoke-FileLinter -PassThru + $res.FailedCount | Should -Be 0 + } + + It 'Should Sum up ' { + #Mock 'Write-FailureStateFile' { Write-Debug 'Mocking Report generator' } + #Mock 'Write-ResultFile' { Write-Debug 'Mocking function Write-ResultFile.' } + $res = Invoke-FileLinter -PassThru + $res.FailedCount | Should -Be 0 + } + } +} + +AfterAll { + $Repo = Get-RepoPath + $RestoreBackup = @{ + Path = Join-Path -Path $Repo.Build.Path -ChildPath 'FileLinter.backup' + Destination = $Repo.Build.FileLinterLogPath + } + Move-Item @RestoreBackup -Force +} diff --git a/src/FileLinter/Invoke-FileLinter.ps1 b/src/FileLinter/Invoke-FileLinter.ps1 new file mode 100644 index 0000000..5b69907 --- /dev/null +++ b/src/FileLinter/Invoke-FileLinter.ps1 @@ -0,0 +1,115 @@ +function Invoke-FileLinter { + <# + .SYNOPSIS + Runs the file linter for all src files found in current repository. + + .DESCRIPTION + Invoke-FileLinter runs the basic file tests and generates a report file for furher usage in the + drone pipeline. + + .INPUTS + [None] + + .OUTPUTS + [DroneHelper.FileLinter.Report] + + .EXAMPLE + Invoke-FileLinter + + .NOTES + #> + + [CmdletBinding()] + [OutputType('DroneHelper.FileLinter.Report')] + + param ( + [Parameter(Mandatory = $false)] + [switch]$PassThru + ) + + begin { + } + + process { + $FileSet = @() + $Repo = Get-RepoPath + $RawFiles = (Get-ChildItem -Path $Repo.src.Path -Recurse -File).FullName + Write-Debug -Message ('EXCLUDE Filter. {0}' -f $Env:EXCLUDE) + if ($Env:EXCLUDE) { + $Files = $RawFiles -notmatch $Env:EXCLUDE + Write-Debug -Message ('Raw File List: {0} | Filtered Files: {1}' -f $RawFiles.Count, $Files.Count) + } + else { + $Files = $RawFiles + } + foreach ($file in $Files) { + Write-Verbose -Message ('Running FileLinter tests for: {0}' -f $file) + $FileResults = [PSCustomObject]@{ + Name = $file + FailedCount = 0 + Tests = [ordered]@{ + Encoding = (Test-FileEncoding -Path $file) + BOM = (Test-FileBOM -Path $file) + EOL = (Test-FileEOL -Path $file) + EOF = (Test-FileEOF -Path $file) + TAB = (Test-FileTab -Path $file) + TailingWhite = (Test-FileTailingWhitespace -Path $file) + } + } + Write-Verbose -Message ('Populating property FailedCount for current file.') + foreach ($item in $FileResults.Tests.Keys.GetEnumerator()) { + if (($FileResults.Tests.$item) -ne $true) { + $FileResults.FailedCount++ + } + } + $FileSet += [PSCustomObject]$FileResults + } + $LinterReport = [PSCustomObject]@{ + Success = $null + FilesCount = ($FileSet | Measure-Object).Count + FailedCount = 0 + Files = $FileSet + + } + Write-Verbose -Message ('Populating total FailedCount property.') + foreach ($i in $LinterReport.Files.FailedCount) { + if ($i -ne 0) { + $LinterReport.FailedCount = $LinterReport.FailedCount + $i + } + } + if ($LinterReport.FailedCount -eq 0) { + $LinterReport.Success = $true + } + else { + $LinterReport.Success = $false + } + $LinterReport.PSObject.TypeNames.Insert(0, 'DroneHelper.FileLinter.Report') + + $LinterReport.Files | Out-String | Write-Verbose -Verbose + + $LinterReport | Format-Table -Property @( + 'Success', + 'FilesCount', + 'FailedCount' + ) | Out-String | Write-Verbose -Verbose + + + $ResultParams = @{ + Type = 'FileLinter' + Path = $Repo.Build.FileLinterLogPath + InputObject = $LinterReport + } + Write-ResultFile @ResultParams + + if (-not ($LinterReport.Success)) { + Write-FailureStateFile -StepName 'FileLinter' + throw 'FileLinter failed!' + } + } + + end { + if ($PassThru.IsPresent) { + Write-Output $LinterReport + } + } +} diff --git a/src/FileLinter/Test-FileBOM.Tests.ps1 b/src/FileLinter/Test-FileBOM.Tests.ps1 new file mode 100644 index 0000000..82070c4 --- /dev/null +++ b/src/FileLinter/Test-FileBOM.Tests.ps1 @@ -0,0 +1,37 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force + #$srcFile = Join-Path -Path $Repo.Src.Path -ChildPath 'FileLinter/Test-FileBOM.ps1' + #. "$srcFile" +} +Describe 'Test-FileBOM' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Test-FileBOM' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Test-FileBOM' } | Should -Not -Throw + } + + It 'Help Content' { + $foo = Get-Help -Name 'Test-FileBOM' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } + Context 'Unit tests' -Tag 'Unit' { + It 'Should throw' { + { Test-FileBOM -foo } | Should -Throw + } + + It 'Should not throw' { + { Test-FileBOM -Path $Repo.Src.Manifest.Item.FullName } | Should -Not -Throw + } + + It 'Should return true' { + Test-FileBOM -Path $Repo.Src.Manifest.Item.FullName | Should -Be $true + } + } +} diff --git a/src/FileLinter/Test-FileBOM.ps1 b/src/FileLinter/Test-FileBOM.ps1 new file mode 100644 index 0000000..2d1c04e --- /dev/null +++ b/src/FileLinter/Test-FileBOM.ps1 @@ -0,0 +1,61 @@ +function Test-FileBOM { + <# + .SYNOPSIS + Tests given file if native utf8 w/o BOM is used. Returns false if BOM is present. + + .DESCRIPTION + This function is used to test for a valid encoding without BOM. + + .PARAMETER Path + Full or relative path to existing file. + + .INPUTS + [None] + + .OUTPUTS + [bool] + + .EXAMPLE + Test-FileBOM -Path './Testfile.txt' + + .NOTES + #> + + [CmdletBinding()] + [OutputType([bool])] + + param ( + [Parameter(Mandatory = $true)] + [ValidateScript( + { + Test-Path -Path $_ + } + )] + [string]$Path + ) + + begin { + } + + process { + try { + $contents = [byte[]]::new(3) + $stream = [System.IO.File]::OpenRead($Path) + $stream.Read($contents, 0, 3) | Out-Null + $stream.Close() + } + catch { + Write-Error -Message 'Could not read the given file!' -ErrorAction 'Stop' + } + Write-Debug -Message ('BOM Content was: {0}' -f ([System.BitConverter]::ToString($contents))) + if ( $contents[0] -eq 0xEF -and $contents[1] -eq 0xBB -and $contents -eq 0xBF ) { + Write-Output $false + } + else { + Write-Output $true + } + } + + end { + } +} diff --git a/src/FileLinter/Test-FileEOF.Tests.ps1 b/src/FileLinter/Test-FileEOF.Tests.ps1 new file mode 100644 index 0000000..37e46fc --- /dev/null +++ b/src/FileLinter/Test-FileEOF.Tests.ps1 @@ -0,0 +1,37 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force + #$srcFile = Join-Path -Path $Repo.Src.Path -ChildPath 'FileLinter/Test-FileEOF.ps1' + #. "$srcFile" + #$srcFile = Join-Path -Path $Repo.Src.Path -ChildPath 'FileLinter/Test-FileEOL.ps1' + #. "$srcFile" +} +Describe 'Test-FileEOF' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Test-FileEOF' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Test-FileEOF' } | Should -Not -Throw + } + + It 'Help Content' { + $foo = Get-Help -Name 'Test-FileEOF' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } + Context 'Coding tests' -Tag 'Unit' { + It 'Should throw' { + { Test-FileEOF -foo } | Should -Throw + } + It 'Should not throw' { + { Test-FileEOF -Path $Repo.Src.Manifest.Item.FullName } | Should -Not -Throw + } + It 'Should be true' { + Test-FileEOF -Path $Repo.Src.Manifest.Item.FullName | Should -Be $true + } + } +} diff --git a/src/FileLinter/Test-FileEOF.ps1 b/src/FileLinter/Test-FileEOF.ps1 new file mode 100644 index 0000000..e24eeba --- /dev/null +++ b/src/FileLinter/Test-FileEOF.ps1 @@ -0,0 +1,56 @@ +function Test-FileEOF { + <# + .SYNOPSIS + Returns false if EOF isn't an empty line. + + .DESCRIPTION + Test the given file against the EOF standard (final empty/blank line + CRLF) and returns true or false. + + .PARAMETER Path + Relative or full path to an existing file. + + .INPUTS + [none] + + .OUTPUTS + [bool] + + .EXAMPLE + Test-FileEOF -Path './testfile.txt' + + .NOTES + #> + + [CmdletBinding()] + [OutputType([bool])] + + param ( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + begin { + } + + process { + if (-not (Test-FileEOL -Path $Path)) { + Write-Warning -Message ('The given file does not use CRLF! ({0})' -f $Path) + Write-Output $false + } + $content = Get-Content -Path $Path -Raw -Encoding 'utf8' + $lastLine = ($content -split "`r`n")[-1].Length + # Test for multiple lines without content on EOF + $perLine = ($content -split "`r`n")[-2].Length + if (($lastLine -eq 0) -and ($perLine -ne 0)) { + Write-Debug -Message ('EOF: LastLine {0}; PenultimateLine {1} -> true' -f $lastLine, $perLine) + Write-Output $true + } + else { + Write-Debug -Message ('EOF: LastLine {0}; PenultimateLine {1} -> false' -f $lastLine, $perLine) + Write-Output $false + } + } + + end { + } +} diff --git a/src/FileLinter/Test-FileEOL.Tests.ps1 b/src/FileLinter/Test-FileEOL.Tests.ps1 new file mode 100644 index 0000000..6834ce9 --- /dev/null +++ b/src/FileLinter/Test-FileEOL.Tests.ps1 @@ -0,0 +1,38 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force + #$srcFile = Join-Path -Path $Repo.Src.Path -ChildPath 'FileLinter/Test-FileEOL.ps1' + #. "$srcFile" +} +Describe 'Test-FileEOL' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Test-FileEOL' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Test-FileEOL' } | Should -Not -Throw + } + + It 'Help Content' { + $foo = Get-Help -Name 'Test-FileEOL' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } + Context 'Coding tests' -Tag 'Unit' { + It 'Should throw' { + { Test-FileEOL -foo } | Should -Throw + } + It 'Should not throw' { + { Test-FileEOL -Path $Repo.Src.Manifest.Item.FullName } | Should -Not -Throw + } + It 'Should be true' { + Test-FileEOL -Path $Repo.Src.Manifest.Item.FullName | Should -Be $true + } + It 'Should be false' { + Test-FileEOL -Path $Repo.Changelog.Path | Should -Be $false + } + } +} diff --git a/src/FileLinter/Test-FileEOL.ps1 b/src/FileLinter/Test-FileEOL.ps1 new file mode 100644 index 0000000..1eff1bf --- /dev/null +++ b/src/FileLinter/Test-FileEOL.ps1 @@ -0,0 +1,61 @@ +function Test-FileEOL { + <# + .SYNOPSIS + Returns false if EOL isn't CRLF + + .DESCRIPTION + Tests given file against valid EOL. Returns true if CRLF is used. + + .PARAMETER Path + Relative or full path to an existing file. + + .INPUTS + [None] + + .OUTPUTS + [bool] + + .EXAMPLE + Test-FileEOL -Path './TestFile.txt' + + .NOTES + #> + + [CmdletBinding()] + [OutputType([bool])] + + param ( + [Parameter(Mandatory = $true)] + [ValidateScript( + { + Test-Path -Path $_ + } + )] + [string]$Path + ) + + begin { + } + + process { + $content = Get-Content -Path $Path -Raw -Encoding 'utf8' + $CRLFCount = ([regex]::Matches($content, "`r`n$")).Count + $LFCount = ([regex]::Matches($content, "`n$")).Count + + if ($CRLFCount -eq $LFCount) { + Write-Debug -Message 'EOL: CRLFCount = LFCount -> true' + Write-Output $true + } + elseif ($CRLFCount -gt $LFCount) { + Write-Debug -Message 'EOL: CRLFCount > LFCount -> false' + Write-Output $false + } + elseif ($LFCount -gt $CRLFCount) { + Write-Debug -Message 'EOL: CRLFCount < LFCount -> false' + Write-Output $false + } + } + + end { + } +} diff --git a/src/FileLinter/Test-FileEncoding.Tests.ps1 b/src/FileLinter/Test-FileEncoding.Tests.ps1 new file mode 100644 index 0000000..055d531 --- /dev/null +++ b/src/FileLinter/Test-FileEncoding.Tests.ps1 @@ -0,0 +1,41 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force + #$srcFile = Join-Path -Path $Repo.Src.Path -ChildPath 'FileLinter/Test-FileEncoding.ps1' + #. "$srcFile" +} +Describe 'Test-FileEncoding' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Test-FileEncoding' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Test-FileEncoding' } | Should -Not -Throw + } + + It 'Help Content' { + $foo = Get-Help -Name 'Test-FileEncoding' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } + Context 'Coding tests' -Tag 'Unit' { + It 'Should throw' { + { Test-FileEncoding -foo } | Should -Throw + } + It 'Should not throw' { + { Test-FileEncoding -Path $Repo.Src.Manifest.Item.FullName } | Should -Not -Throw + } + + It 'Should return true' { + { Test-FileEncoding -Path $Repo.Src.Manifest.Item.FullName } | Should -Be $true + } + It 'UTF-8 test file should return true' { + Test-FileEncoding -Path ( + Join-Path -Path $repo.Resources.Path -ChildPath 'TestData/encodingTest.txt' + ) | Should -BeTrue + } + } +} diff --git a/src/FileLinter/Test-FileEncoding.ps1 b/src/FileLinter/Test-FileEncoding.ps1 new file mode 100644 index 0000000..d7279b6 --- /dev/null +++ b/src/FileLinter/Test-FileEncoding.ps1 @@ -0,0 +1,73 @@ +function Test-FileEncoding { + <# + .SYNOPSIS + Returns true if the given file is written in a valid encoding + + .DESCRIPTION + Test the given file against the encoding regex and returns true or false + + .PARAMETER Path + Relative or full path to an existing file. + + .PARAMETER Encoding + Optional custom encoding regex string. Default is (utf8|ascii|xml). + + .INPUTS + [none] + + .OUTPUTS + [bool] + + .EXAMPLE + Test-FileEncoding -Path './testfile.txt' + + .NOTES + #> + + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingInvokeExpression', + '', + Justification = 'static input without user manipulation' + )] + [OutputType([bool])] + + param ( + [Parameter(Mandatory = $true)] + [ValidateScript( + { + Test-Path -Path $_ + } + )] + [string]$Path, + + [Parameter(Mandatory = $false)] + [string]$Encoding = '(utf8|utf-8|ascii|xml)' + ) + + begin { + } + + process { + try { + Get-Command -Name 'file' -ErrorAction 'Stop' | Out-Null + } + catch { + Write-Error -Message "Could not find command called 'file'!" -ErrorAction 'Stop' + } + $Res = Invoke-Expression -Command ("file '{0}' " -f $Path) + # Remove the file from matching. Use the latest array element if split doesn't work. + $ParsedResult = ($Res -split ':')[-1] + Write-Debug -Message ('Encoding: Raw file output {0}' -f $Res) + Write-Debug -Message ('Parsed match string: {0}' -f $ParsedResult) + if ($ParsedResult -match $Encoding) { + Write-Output $true + } + else { + Write-Output $false + } + } + + end { + } +} diff --git a/src/FileLinter/Test-FileTab.Tests.ps1 b/src/FileLinter/Test-FileTab.Tests.ps1 new file mode 100644 index 0000000..0c6c7c0 --- /dev/null +++ b/src/FileLinter/Test-FileTab.Tests.ps1 @@ -0,0 +1,36 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force + #$srcFile = Join-Path -Path $Repo.Src.Path -ChildPath 'FileLinter/Test-FileTab.ps1' + #. "$srcFile" +} +Describe 'Test-FileTab' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Test-FileTab' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Test-FileTab' } | Should -Not -Throw + } + + It 'Help Content' { + $foo = Get-Help -Name 'Test-FileTab' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } + Context 'Coding tests' -Tag 'Unit' { + It 'Should throw' { + { Test-FileTab -foo } | Should -Throw + } + It 'Should not throw' { + { Test-FileTab -Path $Repo.Src.Manifest.Item.FullName } | Should -Not -Throw + } + + It 'Should be true' { + Test-FileTab -Path $Repo.Src.Manifest.Item.FullName | Should -Be $true + } + } +} diff --git a/src/FileLinter/Test-FileTab.ps1 b/src/FileLinter/Test-FileTab.ps1 new file mode 100644 index 0000000..fda2887 --- /dev/null +++ b/src/FileLinter/Test-FileTab.ps1 @@ -0,0 +1,56 @@ +function Test-FileTab { + <# + .SYNOPSIS + Returns false if tab char is used in file. + + .DESCRIPTION + Test the given file if tabs are used. Returns false if any tabs were found. + + .PARAMETER Path + elative or full path to an existing file. + + .INPUTS + [none] + + .OUTPUTS + [bool] + + .EXAMPLE + Test-FileTab -Path './testfile.txt' + + .NOTES + #> + + [CmdletBinding()] + [OutputType([bool])] + + param ( + [Parameter(Mandatory = $true)] + [ValidateScript( + { + Test-Path -Path $_ + } + )] + [string]$Path + ) + + begin { + } + + process { + $content = Get-Content -Path $Path -Raw -Encoding 'utf8' + $Tabs = ([regex]::Matches($content, "`t")).Count + + if ($Tabs -ne 0 ) { + Write-Debug -Message ('Tabs: {0} -> false' -f $Tabs) + Write-Output $false + } + else { + Write-Debug -Message ('Tabs: {0} -> true' -f $Tabs) + Write-Output $true + } + } + + end { + } +} diff --git a/src/FileLinter/Test-FileTailingWhitespace.Tests.ps1 b/src/FileLinter/Test-FileTailingWhitespace.Tests.ps1 new file mode 100644 index 0000000..6b1ddcb --- /dev/null +++ b/src/FileLinter/Test-FileTailingWhitespace.Tests.ps1 @@ -0,0 +1,36 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force + #$srcFile = Join-Path -Path $Repo.Src.Path -ChildPath 'FileLinter/Test-FileTailingWhitespace.ps1' + #. "$srcFile" +} +Describe 'Test-FileTailingWhitespace' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Test-FileTailingWhitespace' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Test-FileTailingWhitespace' } | Should -Not -Throw + } + + It 'Help Content' { + $foo = Get-Help -Name 'Test-FileTailingWhitespace' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } + Context 'Coding tests' -Tag 'Unit' { + It 'Should throw' { + { Test-FileTailingWhitespace -foo } | Should -Throw + } + It 'Should not throw' { + { Test-FileTailingWhitespace -Path $Repo.Src.Manifest.Item.FullName } | Should -Not -Throw + } + + It 'Should be true' { + Test-FileTailingWhitespace -Path $Repo.Src.Manifest.Item.FullName | Should -Be $true + } + } +} diff --git a/src/FileLinter/Test-FileTailingWhitespace.ps1 b/src/FileLinter/Test-FileTailingWhitespace.ps1 new file mode 100644 index 0000000..574b094 --- /dev/null +++ b/src/FileLinter/Test-FileTailingWhitespace.ps1 @@ -0,0 +1,62 @@ +function Test-FileTailingWhitespace { + <# + .SYNOPSIS + Returns false if there are any tailing whitespace in lines. + + .DESCRIPTION + Tests the given file for tailing whitespace. Returns true if not found. + + .PARAMETER Path + Relative or full path to an existing file. + + .INPUTS + [none] + + .OUTPUTS + [bool] + + .EXAMPLE + Test-FileTailingWhitespace.ps1 -Path './testfile.txt' + + .NOTES + #> + + [CmdletBinding()] + [OutputType([Bool])] + + param ( + [Parameter(Mandatory = $true)] + [ValidateScript( + { + Test-Path -Path $_ + } + )] + [string]$Path + ) + + begin { + } + + process { + $content = Get-Content -Path $Path -Encoding 'utf8' + $WhiteSpace = 0 + foreach ($line in $content) { + $c = ([regex]::Matches($line, "\s+$")).Count + if ( $c -gt 0 ) { + $WhiteSpace++ + } + } + + if ($WhiteSpace -ne 0 ) { + Write-Debug -Message ('WhiteSpace: {0} -> false' -f $WhiteSpace) + Write-Output $false + } + else { + Write-Debug -Message ('WhiteSpace: {0} -> true' -f $WhiteSpace) + Write-Output $true + } + } + + end { + } +} diff --git a/src/Formats/.gitkeep b/src/Formats/.gitkeep new file mode 100644 index 0000000..98ea7c2 --- /dev/null +++ b/src/Formats/.gitkeep @@ -0,0 +1 @@ +git placeholder file diff --git a/src/Helper/Get-RepoPath.Tests.ps1 b/src/Helper/Get-RepoPath.Tests.ps1 new file mode 100644 index 0000000..d1bf35a --- /dev/null +++ b/src/Helper/Get-RepoPath.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Get-RepoPath' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Get-RepoPath' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Get-RepoPath' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Get-RepoPath' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/Helper/Get-RepoPath.ps1 b/src/Helper/Get-RepoPath.ps1 new file mode 100644 index 0000000..6bd8d02 --- /dev/null +++ b/src/Helper/Get-RepoPath.ps1 @@ -0,0 +1,167 @@ +function Get-RepoPath { + <# + .SYNOPSIS + Updates the module manifest file fields to prepare the new build. + + .DESCRIPTION + Replaces the version fields in the manifest file. Uses Drone env vars populated by pushed tags. + + .Parameter SubPath + An optional string array of sub directories relative to the root. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [DroneHelper.Repo.Path] Returns a folder structured like object with relevant full paths.s + + .EXAMPLE + Import-Module -Name DroneHelper; Get-RepoPath + #> + [CmdletBinding()] + [OutputType('DroneHelper.Repo.Path')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseConsistentWhitespace', + '', + Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' + )] + param ( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [String[]]$SubPath + ) + + process { + $root = Split-Path -Path (Get-GitDirectory) + + $BaseName = Get-Item -Path ('{0}/src/*.psd1' -f $root) | Select-Object -ExpandProperty 'BaseName' + + $failureLogPath = Join-Path -Path $root -ChildPath 'failure.log' + + # *.psd1 related + $manifestPath = Join-Path -Path $root -ChildPath 'src/*.psd1' + $manifest = Get-Item -Path $manifestPath + + # *.psm1 related + $scriptModulePath = Join-Path -Path $root -ChildPath 'src/*.psm1' + $scriptModule = Get-Item -Path $scriptModulePath + + # Subdir related + $srcPath = Join-Path -Path $root -ChildPath 'src' + $binPath = Join-Path -Path $root -ChildPath 'bin' + $buildPath = Join-Path -Path $root -ChildPath 'build' + $resourcePath = Join-Path -Path $root -ChildPath 'resources' + + # bin + build artifact related + $mergedScriptModulePath = Join-Path -Path $binPath -ChildPath $scriptModule.Name + + $artifactName = '{0}.zip' -f $BaseName + $artifactPath = Join-Path -Path $binPath -ChildPath $artifactName + $expandPath = Join-Path -Path $binPath -ChildPath $BaseName + + # iteration through the optional sub paths + $formatPath = Join-Path -Path $srcPath -ChildPath 'Formats/' + $cachePath = Join-Path -Path $srcPath -ChildPath 'Cache/' + + $docsPath = Join-Path -Path $root -ChildPath 'docs' + $modulePagePath = Join-Path -Path $docsPath -ChildPath 'README.md' + $docsMarkdownFilter = Join-Path -Path $docsPath -ChildPath '*.md' + + $subDir = @{} + foreach ($dir in $SubPath) { + $subDir.$dir = Join-Path -Path $root -ChildPath $dir + } + + $changelogPath = Join-Path -Path $root -ChildPath 'CHANGELOG.md' + $changelogExits = Test-Path -Path $changelogPath + + $ps1Filter = Join-Path -Path $srcPath -ChildPath '*.ps1' + + $pesterLogPath = Join-Path -Path $buildPath -ChildPath 'Pester-Results.log' + $scriptAnalyzerLogPath = Join-Path -Path $buildPath -ChildPath 'ScriptAnalyzer-Results.log' + $fileLinterLogPath = Join-Path -Path $buildPath -ChildPath 'FileLinter-Results.log' + + $scriptAnalyzerSettingsPath = Join-Path -Path $resourcePath -ChildPath 'PSScriptAnalyzerSettings.psd1' + + # DroneHelper Module specific + + $droneModuleBase = $MyInvocation.MyCommand.Module.ModuleBase + $PathParams = @{ + Path = $droneModuleBase + ChildPath = 'Rules/PSScriptAnalyzerSettings.psd1' + } + $droneAnalyzerDefaultPath = Join-Path @PathParams + + if ($changelogExits) { + $changelog = Get-Item -Path $changelogPath + } + else { + $changelog = $null + } + + $Path = [PSCustomObject]@{ + Artifact = $BaseName + Root = $root + + Src = [PSCustomObject]@{ + Path = $srcPath + Manifest = [PSCustomObject] @{ + Path = $manifestPath + Item = $manifest + } + ScriptModule = [PSCustomObject]@{ + Path = $scriptModulePath + Item = $scriptModule + } + Formats = [PSCustomObject]@{ + Path = $formatPath + Exists = Test-Path -Path $formatPath + } + Cache = [PSCustomObject]@{ + Path = $cachePath + Exists = Test-Path -Path $cachePath + } + PS1Filter = $ps1Filter + } + Bin = [PSCustomObject]@{ + Path = $binPath + ScriptModuleName = $mergedScriptModulePath + ArtifactName = $artifactName + ArtifactPath = $artifactPath + ExpandPath = $expandPath + + } + Build = [PSCustomObject]@{ + Path = $buildPath + PesterLogPath = $pesterLogPath + ScriptAnalyzerLogPath = $scriptAnalyzerLogPath + FileLinterLogPath = $fileLinterLogPath + } + Changelog = [PSCustomObject]@{ + Path = $changelogPath + Exists = $changelogExits + Item = $changelog + } + Docs = [PSCustomObject]@{ + Path = $docsPath + ModulePagePath = $modulePagePath + MarkdownFilter = $docsMarkdownFilter + } + DroneHelper = [PSCustomObject]@{ + ModuleBase = $MyInvocation.MyCommand.Module.ModuleBase + ScriptAnalyzerDefaultsPath = $droneAnalyzerDefaultPath + } + Resources = [PSCustomObject]@{ + Path = $resourcePath + ScriptAnalyzerSettingsPath = $scriptAnalyzerSettingsPath + ScriptAnalyzerSettingsExist = Test-Path -Path $scriptAnalyzerSettingsPath + + } + FailureLogPath = $failureLogPath + SubDir = $subDir + } + $Path.PSObject.TypeNames.Insert(0, 'DroneHelper.Repo.Path') + Write-Output -InputObject $Path + } + +} diff --git a/src/Helper/Set-EOL.Tests.ps1 b/src/Helper/Set-EOL.Tests.ps1 new file mode 100644 index 0000000..a62d18b --- /dev/null +++ b/src/Helper/Set-EOL.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Set-EOL' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Set-EOL' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Set-EOL' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Set-EOL' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/Helper/Set-EOL.ps1 b/src/Helper/Set-EOL.ps1 new file mode 100644 index 0000000..d89d880 --- /dev/null +++ b/src/Helper/Set-EOL.ps1 @@ -0,0 +1,59 @@ +function Set-EOL { + <# + .SYNOPSIS + Helper function to set the EOL sequence to LF or CRLF. + + .DESCRIPTION + Helper for changing the EOL independent to the current OS defaults. + + .PARAMETER Style + Optional style parameter for `unix` or `win.`. Default is `unix`. + + .PARAMETER Path + Mandatory path for target file. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [DroneHelper.Repo.Path] Returns a folder structured like object with relevant full paths.s + + .EXAMPLE + Import-Module -Name DroneHelper; Set-EOL -Path './Readme.md' + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'system state does not change permanent in temp build clients.' + )] + param ( + [Parameter(Mandatory = $false)] + [ValidateSet('unix', 'win')] + [String]$Style = 'unix', + + [Parameter(Mandatory = $true)] + [System.IO.FileInfo]$Path + ) + + process { + if (!(Test-Path $Path.FullName)) { + Write-Error -Message ('{0} not found!' -f $Path.FullName) -ErrorAction Stop + } + switch ($Style) { + 'unix' { + $eol = "`n" + Write-Verbose -Message ('Reading {0}' -f $Path.FullName) + $text = [IO.File]::ReadAllText($Path.FullName) -replace "`r`n", $eol + Write-Debug -Message $text + } + 'win' { + $eol = "`r`n" + $text = [IO.File]::ReadAllText($Path.FullName) -replace "`n", $eol + } + } + Write-Verbose -Message ("Writing back {0}" -f $Path.FullName) + [IO.File]::WriteAllText($Path.FullName, $text) + + } +} diff --git a/src/ModuleRoot.psm1 b/src/ModuleRoot.psm1 new file mode 100644 index 0000000..e72fffe --- /dev/null +++ b/src/ModuleRoot.psm1 @@ -0,0 +1,6 @@ +# Get all child items in the Script path and exclude the Deploy script (if present.) +$Functions = Get-ChildItem -Path $PSScriptRoot\*.ps1 -Recurse | Where-Object { $_.BaseName -notmatch '.Tests' } + +ForEach ($Item in $Functions) { + . $Item.FullName +} diff --git a/src/PRComment/Send-PRComment.Tests.ps1 b/src/PRComment/Send-PRComment.Tests.ps1 new file mode 100644 index 0000000..885572b --- /dev/null +++ b/src/PRComment/Send-PRComment.Tests.ps1 @@ -0,0 +1,41 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Send-PRComment' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Send-PRComment' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Send-PRComment' } | Should -Not -Throw + } + + It 'Help Content' { + $foo = Get-Help -Name 'Send-PRComment' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } + + Context 'Unit tests' -Tag 'Unit' { + It 'Should throw' { + { Send-PRComment -foo } | Should -Throw + } + # + #It 'Should not throw' { + #Mock 'FunctionName' -ModuleName $Repo.Artifact {} + # { Send-PRComment } | Should -Not -Throw + #} + } + + <# + Context 'Integration tests' -Tag 'Integration' { + It 'Real world example' { + { Send-PRComment } | Should -Should -Be $null + } + } + #> +} diff --git a/src/PRComment/Send-PRComment.ps1 b/src/PRComment/Send-PRComment.ps1 new file mode 100644 index 0000000..fe5a906 --- /dev/null +++ b/src/PRComment/Send-PRComment.ps1 @@ -0,0 +1,173 @@ +function Send-PRComment { + <# + .SYNOPSIS + Sends build report as Gitea PR comment. + + .DESCRIPTION + Send-PRComment is used to report the build details from drone.io pipeline. + + .PARAMETER Mode + Sets the report mode. Default is 'Renew'. This mode deletes the old pr comments and creates a new onw. + Also available: + - 'Add' -> simply adds new pr comments. + - 'Edit' -> Edits the last known pr comment. Doesn't clean old ones. + + .INPUTS + [None]. + + .OUTPUTS + [None] + + .EXAMPLE + Send-PRComment + Depends on Drone.IO injected environment vars. Doesn't work locally on dev systems. + .NOTES + #> + + [CmdletBinding()] + #[OutputType([string])] + param ( + [Parameter(Mandatory = $false, HelpMessage = 'HelpMessage')] + [ValidateSet('Add', 'Edit', 'Renew')] + [string]$Mode = 'Renew' + ) + + begin { + } + + process { + $Repo = Get-RepoPath + $Workspace = $Repo.Root + Write-Debug -Message ('Workspace: {0}' -f $Workspace) + $PRCommentFile = Join-Path -Path $Workspace -ChildPath 'pr_comment.log' + Write-Debug -Message ('PRCommentFile: {0}' -f $PRCommentFile) + $PipelineStateFile = Join-Path -Path $Workspace -ChildPath 'failure.log' + Write-Debug -Message ('PipelineStateFile: {0}' -f $PipelineStateFile) + + Write-Debug -Message ('CUSTOM_PIPELINE_STATE: {0}' -f $Env:CUSTOM_PIPELINE_STATE) + if ($Env:CUSTOM_PIPELINE_STATE -eq $true) { + if (Test-Path $PipelineStateFile) { + Write-Debug -Message ('Setting custom pipeline status to failed') + $PipelineState = 'failed' + } + else { + Write-Debug -Message ('Setting custom pipeline status to success') + $PipelineState = 'success' + } + } + else { + Write-Debug -Message ('Setting global drone status {0}' -f $Env:DRONE_BUILD_STATUS) + $PipelineState = $Env:DRONE_BUILD_STATUS + } + + if ($Env:GITEA_BASE) { + $GiteaBase = $Env:GITEA_BASE + } + else { + $GiteaBase = 'https://gitea.ocram85.com' + } + + $APIHeaders = @{ + accept = 'application/json' + 'Content-Type' = 'application/json' + } + + # Can be used with POST method to add new comment. Used with GET method returns all comments. + $CommentAPICall = ('{0}/api/v1/repos/{1}/{2}/issues/{3}/comments?access_token={4}' -f + $GiteaBase, + $Env:DRONE_REPO_OWNER, + $Env:DRONE_REPO_NAME, + $Env:DRONE_PULL_REQUEST, + $Env:GITEA_TOKEN + ) + + # Update Comment API endpoint: 0 - GiteaBase, 1 - Owner, 2- Repo, 3 - PR, 4 - Token + # Method Delete - removes the given comment. Patch - updates the given comment. + $UpdateAPICall = '{0}/api/v1/repos/{1}/{2}/issues/comments/{3}?access_token={4}' + + + if ($Mode -eq 'Renew') { + $Comments = Invoke-RestMethod -Method 'Get' -Headers $APIHeaders -Uri $CommentAPICall + $DroneComments = $Comments | Where-Object { + $_.user.login -eq 'drone' + } | Select-Object -ExpandProperty 'id' + Write-Debug -Message ('Found Drone comments: {0}.' -f ($DroneComments -join ', ')) + foreach ($id in $DroneComments) { + $ExtAPI = $UpdateAPICall -f @( + $GiteaBase, + $Env:DRONE_REPO_OWNER, + $Env:DRONE_REPO_NAME, + $id, + $Env:GITEA_TOKEN + ) + Write-Debug -Message ('Exec API Call: {0}' -f $ExtAPI) + Invoke-RestMethod -Method 'Delete' -Headers $APIHeaders -Uri $ExtAPI + } + } + + if ($Mode -eq 'Edit') { + $Comments = Invoke-RestMethod -Method 'Get' -Headers $APIHeaders -Uri $CommentAPICall + $DroneComments = $Comments | Where-Object { + $_.user.login -eq 'drone' + } | Select-Object -ExpandProperty 'id' + Write-Debug -Message ('Found Drone comments: {0}.' -f ($DroneComments -join ', ')) + $EditId = $DroneComments | Sort-Object | Select-Object -Last 1 + Write-Debug -Message ('Edit Comment with id {0}' -f $EditId) + } + + $PRCommentHeader = ('> Drone.io PR Build No. [#{0}]({1}://{2}/{3}/{4}): ``{5}``' -f + $Env:DRONE_BUILD_NUMBER, + $Env:DRONE_SYSTEM_PROTO, + $Env:DRONE_SYSTEM_HOST, + $Env:DRONE_REPO, + $Env:DRONE_BUILD_NUMBER, + $PipelineState + + ) + $PRCommentHeader | Out-File -FilePath $PRCommentFile -Encoding 'utf8' + + $LogFiles = (Get-ChildItem -Path $Env:LOG_FILES -File).FullName + foreach ($file in $LogFiles) { + if (Test-Path -Path $file) { + ('#### ``{0}``' -f $file) | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' + $fileContent = Get-Content -Path $file -Raw -Encoding utf8 + $fileContent | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' + [Environment]::NewLine | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' -NoNewline + + } + else { + Write-Warning -Message ('Given file {0} not found!' -f $file) + ('##### ``{0}`` not found!' -f $file) | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' + } + } + + if ($Mode -eq 'Edit') { + 'Last mod: {0}' -f (Get-Date -Format 'u') | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' + } + 'end.' | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' + + $PRCommentJSON = ConvertTo-Json -InputObject @{ + Body = Get-Content -Path $PRCommentFile -Encoding utf8 -Raw + } + Write-Debug -Message ('PR JSON body has a size of {0} chars' -f $PRCommentJSON.length) + + if ($Mode -ne 'Edit') { + Write-Debug -Message 'Adding new Comment.' + Invoke-RestMethod -Method 'Post' -Headers $APIHeaders -Uri $CommentAPICall -Body $PRCommentJSON + } + else { + $ExtAPI = $UpdateAPICall -f @( + $GiteaBase, + $Env:DRONE_REPO_OWNER, + $Env:DRONE_REPO_NAME, + $EditId, + $Env:GITEA_TOKEN + ) + Write-Debug -Message 'Edit last comment.' + Invoke-RestMethod -Method 'Patch' -Headers $APIHeaders -Uri $ExtAPI -Body $PRCommentJSON + } + } + + end { + } +} diff --git a/src/PSModule.Tests.ps1 b/src/PSModule.Tests.ps1 new file mode 100644 index 0000000..2338087 --- /dev/null +++ b/src/PSModule.Tests.ps1 @@ -0,0 +1,14 @@ +Describe 'Test module meta' { + It 'Test manifest file' { + $ManifestFile = (Get-Item -Path "./src/*.psd1").FullName + Test-ModuleManifest -Path $ManifestFile | Should -Be $true + } + It 'Import Module' { + $ManifestFile = (Get-Item -Path "./src/*.psd1").FullName + { Import-Module $ManifestFile } | Should -Not -Throw + } + # Dummy test to force pester error + #It 'Force Pester Error' { + # $true | Should -BeFalse + #} +} diff --git a/src/PSScriptAnalyzer/Invoke-Linter.Tests.ps1 b/src/PSScriptAnalyzer/Invoke-Linter.Tests.ps1 new file mode 100644 index 0000000..0db1814 --- /dev/null +++ b/src/PSScriptAnalyzer/Invoke-Linter.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Invoke-Linter' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Invoke-Linter' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Invoke-Linter' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Invoke-Linter' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/PSScriptAnalyzer/Invoke-Linter.ps1 b/src/PSScriptAnalyzer/Invoke-Linter.ps1 new file mode 100644 index 0000000..e81384e --- /dev/null +++ b/src/PSScriptAnalyzer/Invoke-Linter.ps1 @@ -0,0 +1,71 @@ +function Invoke-Linter() { + <# + .SYNOPSIS + Runs all PSScriptAnalyzer Rules within this repo. + + .DESCRIPTION + This Cmdlet is used in Drone pipeline to run the PSScriptAnalyzer rules.. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] No pipeline output. + + .EXAMPLE + Invoke-Linter + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseConsistentWhitespace', + '', + Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' + )] + param() + process { + $Repo = Get-RepoPath + # Use repo local defaults. if not present use the DroneHelper included as defaults. + if ($Repo.Resources.ScriptAnalyzerSettingsExist) { + $currentRules = $Repo.Resources.ScriptAnalyzerSettingsPath + } + else { + $currentRules = $repo.DroneHelper.ScriptAnalyzerDefaultsPath + } + $AnalyzerParams = @{ + Path = $Repo.Src.Path + Recurse = $true + Settings = $currentRules + Verbose = $VerbosePreference + ReportSummary = $true + } + $AnalyzerResults = Invoke-ScriptAnalyzer @AnalyzerParams + if ( $AnalyzerResults ) { + $AnalyzerResults | Sort-Object -Property @( + "ScriptName", + "Line" + ) | Format-Table @( + "Severity", + "ScriptName", + "Line", + "RuleName", + "Message" + ) -AutoSize | Out-String | Write-Verbose -Verbose + $ResultParams = @{ + Type = 'PSScriptAnalyzer' + Path = $Repo.Build.ScriptAnalyzerLogPath + InputObject = $AnalyzerResults + } + Write-ResultFile @ResultParams + Write-FailureStateFile -StepName 'PSScriptAnalyzer' + throw 'PS Script Analyzer failed!' + } + else { + $ResultParams = @{ + Type = 'Custom' + Path = $Repo.Build.ScriptAnalyzerLogPath + InputObject = ':heavy_check_mark: No violations found.' + } + Write-ResultFile @ResultParams + } + } +} diff --git a/src/Pester/Invoke-UnitTest.Tests.ps1 b/src/Pester/Invoke-UnitTest.Tests.ps1 new file mode 100644 index 0000000..480dc8b --- /dev/null +++ b/src/Pester/Invoke-UnitTest.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Invoke-UnitTest' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Invoke-UnitTest' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Invoke-UnitTest' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Invoke-UnitTest' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/Pester/Invoke-UnitTest.ps1 b/src/Pester/Invoke-UnitTest.ps1 new file mode 100644 index 0000000..4a5f44a --- /dev/null +++ b/src/Pester/Invoke-UnitTest.ps1 @@ -0,0 +1,110 @@ +function Invoke-UnitTest { + <# + .SYNOPSIS + Runs all Pester tests within this repo. + + .DESCRIPTION + This Cmdlet is used in Drone pipeline to perform the Pester based unit tests. + + .PARAMETER CoverageFormat + Pester provides the formats JaCoCo ans CoverageGutters. Default is JaCoCo. + These are the known use cases: + - JaCoCo -> Used as standard coverage report used by sonar + - CoverageGutters -> Custom Format to show coverage in VSCode. + + .PARAMETER Verbosity + This parameter sets the Pester detail level. Default is 'Normal.' Available values are: + 'None', 'Normal', 'Detailed', 'Diagnostic' + + .PARAMETER PassThru + Tells Invoke-UnitTest to write back the Pester results into your variable / output. + + .PARAMETER Tag + Pester build in tag filter as string array. + + .PARAMETER ExcludeTag + Pester build in exclude filter for tests as string array. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] No pipeline output. + + .EXAMPLE + Invoke-UnitTest + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseConsistentWhitespace', + '', + Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' + )] + param ( + [Parameter( Mandatory = $false )] + [ValidateSet('JaCoCo', 'CoverageGutters')] + [string]$CoverageFormat = 'JaCoCo', + + [Parameter(Mandatory = $false)] + [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] + [string]$Verbosity = 'Normal', + + [Parameter(Mandatory = $false)] + [switch]$PassThru, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string[]]$Tag, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string[]]$ExcludeTag + ) + + process { + $Repo = Get-RepoPath + Write-Verbose -Message '===== Running Pester =====' -Verbose:$VerbosePreference + $PesterConf = New-PesterConfiguration + $PesterConf.Run.Path = (Resolve-Path -Path './src').Path + $PesterConf.Run.Exit = $false + $PesterConf.Run.PassThru = $true + $PesterConf.CodeCoverage.Enabled = $true + $PesterConf.CodeCoverage.OutputFormat = $CoverageFormat + $PesterConf.TestResult.Enabled = $true + $CovFiles = Get-ChildItem -Path $Repo.Src.PS1Filter -Recurse | Where-Object { + $_.BaseName -notmatch '.Tests' + } | Select-Object -ExpandProperty 'FullName' + $PesterConf.CodeCoverage.Path = $CovFiles + $PesterConf.Output.Verbosity = $Verbosity + + # Set Tags if given + if ($Tag) { + $PesterConf.Filter.Tag = $Tag + } + if ($ExcludeTag) { + $PesterConf.Filter.ExcludeTag = $ExcludeTag + } + $TestResults = Invoke-Pester -Configuration $PesterConf -ErrorAction 'Stop' + try { + $ResFileParams = @{ + InputObject = $TestResults + Path = $Repo.Build.PesterLogPath + Type = 'Pester' + ErrorAction = 'Stop' + } + Write-ResultFile @ResFileParams + } + catch { + Write-FailureStateFile -StepName 'Pester' + throw ('{0} tests failed!' -f $TestResults.FailedCount) + } + + if ($TestResults.FailedCount -gt 0) { + Write-FailureStateFile -StepName 'Pester' + throw ('{0} tests failed!' -f $TestResults.FailedCount) + } + if ($PassThru.IsPresent) { + Write-Output -InputObject $TestResults + } + } +} diff --git a/src/Reports/Format-FileLinterReport.ps1 b/src/Reports/Format-FileLinterReport.ps1 new file mode 100644 index 0000000..b70264e --- /dev/null +++ b/src/Reports/Format-FileLinterReport.ps1 @@ -0,0 +1,43 @@ +function Format-FileLinterReport { + <# + .SYNOPSIS + Private helper function used by Write-ResultFile. + #> + + [CmdletBinding()] + [OutputType([string])] + + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [PSTypeName('DroneHelper.FileLinter.Report')]$InputObject + ) + + begin { + } + + process { + $Output = @() + if ($InputObject.Success) { + $Output += ':heavy_check_mark: No FileLinter violations in {0} files found.' -f $InputObject.FilesCount + } + else { + $Output += "| Result | File | Failed |" + $Output += "| :----: | :--- | -----: |" + foreach ($file in $InputObject.Files) { + if ($file.FailedCount -gt 0) { + $failedTestNames = ( + $file.Tests.GetEnumerator() | Where-Object { + $_.Value -eq $false + } | Select-Object -ExpandProperty 'Name' + ) -join ', ' + $Output += "| :heavy_exclamation_mark: | ``{0}`` | ``{1}`` |" -f $file.Name, $failedTestNames + } + } + } + Write-Output $Output + } + + end { + } +} diff --git a/src/Reports/Format-PesterReport.ps1 b/src/Reports/Format-PesterReport.ps1 new file mode 100644 index 0000000..f4d2ce6 --- /dev/null +++ b/src/Reports/Format-PesterReport.ps1 @@ -0,0 +1,74 @@ +function Format-PesterReport { + <# + .SYNOPSIS + Private helper function used by Write-ResultFile. + #> + + [CmdletBinding()] + [OutputType([string])] + + param ( + [Parameter(Mandatory = $true)] + [PSCustomObject]$InputObject, + + [Parameter(Mandatory = $false)] + [ValidateSet('Normal', 'Detailed')] + [string]$Verbosity = 'Normal' + ) + + begin { + } + + process { + $Output = @() + $Output += "| Result | Test | Duration |" + $Output += "| :----: | :--- | -------: |" + foreach ($Result in $InputObject.Tests) { + switch ($Result.Result) { + 'Passed' { + if ($Verbosity -eq 'Detailed') { + $RawString = "| :heavy_check_mark: | ``{0}`` | *{1}ms* |" + $Output += $RawString -f $Result.ExpandedPath, $Result.UserDuration.Milliseconds + } + } + 'Failed' { + $RawString = "| :heavy_exclamation_mark: | ``{0}`` | *{1}ms* |" + $Output += $RawString -f $Result.ExpandedPath, $Result.UserDuration.Milliseconds + $Parsed = $Result.ErrorRecord.Exception.Message -split "`n" | Select-Object -First 1 + $Output += "| :fire: | **{0}** | :fire: |" -f $Parsed + } + 'NotRun' { + $RawString = "| :trident: | ``{0}`` | *n/a* |" + $Output += $RawString -f $Result.ExpandedPath + } + Default { + $RawString = "| :warning: | ``{0}`` | *{1}ms* |" + $Output += $RawString -f $Result.ExpandedPath, $Result.UserDuration.Milliseconds + } + } + } + $Output += [Environment]::NewLine + # Writing test result summary + $Output += @( + ':test_tube: **{0}** Total Tests (' -f $InputObject.TotalCount + + ':heavy_check_mark: ``{0} Passed`` :white_small_square:' -f $InputObject.PassedCount + + ':trident: ``{0} Skipped / NotRun`` :white_small_square: ' -f ( + $InputObject.SkippedCount + $InputObject.NotRunCount + ) + + ':warning: ``Unknown`` :white_small_square: ' + + ':heavy_exclamation_mark: ``{0} Failed``)' -f $InputObject.FailedCount + ) + # Writing code coverage summary + # Covered 37,38% / 75%. 610 analyzed Commands in 26 Files. + $Output += @( + ':bookmark_tabs: Covered **{0}%** / ' -f [Math]::Round($InputObject.CodeCoverage.CoveragePercent, 2) + + '{0}%. (' -f $InputObject.CodeCoverage.CoveragePercentTarget + + ':bookmark: ``{0} analyzed Commands`` ' -f $InputObject.CodeCoverage.CommandsAnalyzedCount + + ':page_facing_up: ``in {0} Files``)' -f $InputObject.CodeCoverage.FilesAnalyzedCount + ) + Write-Output $Output + } + + end { + } +} diff --git a/src/Reports/Format-ScriptAnalyzerReport.ps1 b/src/Reports/Format-ScriptAnalyzerReport.ps1 new file mode 100644 index 0000000..0f6e1b7 --- /dev/null +++ b/src/Reports/Format-ScriptAnalyzerReport.ps1 @@ -0,0 +1,40 @@ +function Format-ScriptAnalyzerReport { + <# + .SYNOPSIS + Private helper function used by Write-ResultFile. + #> + + [CmdletBinding()] + [OutputType([string])] + + param ( + [Parameter(Mandatory = $true)] + [PSCustomObject]$InputObject + ) + + begin { + } + + process { + $Output = @() + $Output += "| Severity | ScriptName | Line | RuleName | Message |" + $Output += "| :------: | :--------- | :--: | :------- | :------ |" + + foreach ( $v in $InputObject ) { + switch ($v.Severity) { + 'Warning' { $Emoji = ':warning:' } + 'Error' { $Emoji = ':heavy_exclamation_mark:' } + 'Information' { $Emoji = ':mag:' } + Default { $Emoji = ':fried_egg:' } + } + $RawString = "| {0} | {1} | {2} | {3} | {4} |" + $Output += $RawString -f $Emoji, $v.ScriptName, $v.Line, $v.RuleName, $v.Message + } + $RuleURL = 'https://github.com/PowerShell/PSScriptAnalyzer/tree/master/RuleDocumentation' + $Output += "`n> See [RuleDocumentation]({0}) for additional help.`n" -f $RuleURL + Write-Output $Output + } + + end { + } +} diff --git a/src/Rules/PSScriptAnalyzerSettings.psd1 b/src/Rules/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..0f65e41 --- /dev/null +++ b/src/Rules/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,140 @@ +@{ + Severity = 'Error', 'Warning', 'Information' + + IncludeRules = @( + 'PSAlignAssignmentStatement', + 'PSAvoidAssignmentToAutomaticVariable', + 'PSAvoidDefaultValueForMandatoryParameter', + 'PSAvoidDefaultValueSwitchParameter', + 'PSAvoidGlobalAliases', + 'PSAvoidGlobalFunctions', + 'PSAvoidGlobalVars', + 'PSAvoidInvokingEmptyMembers', + 'PSAvoidLongLines', + 'PSAvoidNullOrEmptyHelpMessageAttribute', + 'PSAvoidOverwritingBuiltInCmdlets', + 'PSAvoidShouldContinueWithoutForce', + 'PSAvoidTrailingWhitespace', + 'PSAvoidUsingCmdletAliases', + 'PSAvoidUsingComputerNameHardcoded', + 'PSAvoidUsingConvertToSecureStringWithPlainText', + 'PSAvoidUsingDeprecatedManifestFields', + 'PSAvoidUsingDoubleQuotesForConstantString', + 'PSAvoidUsingEmptyCatchBlock', + 'PSAvoidUsingInvokeExpression', + 'PSAvoidUsingPlainTextForPassword', + 'PSAvoidUsingPositionalParameters', + 'PSAvoidUsingUsernameAndPasswordParams', + 'PSAvoidUsingWMICmdlet', + 'PSAvoidUsingWriteHost', + 'PSMisleadingBacktick', + 'PSMissingModuleManifestField', + 'PSPlaceCloseBrace', + 'PSPlaceOpenBrace', + 'PSPossibleIncorrectComparisonWithNull', + 'PSPossibleIncorrectUsageOfAssignmentOperator', + 'PSPossibleIncorrectUsageOfRedirectionOperator', + 'PSProvideCommentHelp', + 'PSReservedCmdletChar', + 'PSReservedParams', + 'PSReviewUnusedParameter', + 'PSShouldProcess', + 'PSUseApprovedVerbs', + 'PSUseBOMForUnicodeEncodedFile', + 'PSUseCmdletCorrectly', + # There is no predefined set for Pwsh7 Cmdlets + 'PSUseCompatibleCmdlets', + #'PSUseCompatibleCommands', + 'PSUseCompatibleSyntax', + #'PSUseCompatibleTypes', + 'PSUseConsistentIndentation', + # Disable if bug in 1.19.1 version occurs. + 'PSUseConsistentWhitespace', + 'PSUseCorrectCasing', + 'PSUseDeclaredVarsMoreThanAssignments', + 'PSUseLiteralInitializerForHashtable', + 'PSUseOutputTypeCorrectly', + 'PSUsePSCredentialType', + 'PSUseProcessBlockForPipelineCommand', + 'PSUseShouldProcessForStateChangingFunctions', + 'PSUseSingularNouns', + 'PSUseSupportsShouldProcess', + 'PSUseToExportFieldsInManifest', + 'PSUseUTF8EncodingForHelpFile', + 'PSUseUsingScopeModifierInNewRunspaces' + ) + + Rules = @{ + + PSAvoidLongLines = @{ + Enable = $true + MaximumLineLength = 116 + } + + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + + PSPlaceCloseBrace = @{ + Enable = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + NoEmptyLineBefore = $false + } + + PSProvideCommentHelp = @{ + Enable = $true + ExportedOnly = $false + BlockComment = $true + VSCodeSnippetCorrection = $false + Placement = "begin" + } + + PSUseCompatibleCmdlets = @{ + compatibility = @( + "desktop-5.1.14393.206-windows", + "core-6.1.0-windows" + ) + } + + PSUseCompatibleSyntax = @{ + Enable = $true + TargetVersions = @( + "7.0", + "5.1" + ) + } + + PSUseConsistentIndentation = @{ + Enable = $true + Kind = 'space' + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IndentationSize = 4 + } + + PSUseConsistentWhitespace = @{ + Enable = $true + CheckInnerBrace = $true + CheckOpenBrace = $true + CheckOpenParen = $true + CheckOperator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $false + CheckSeparator = $true + CheckParameter = $false + IgnoreAssignmentOperatorInsideHashTable = $true + } + + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $false + } + + PSUseCorrectCasing = @{ + Enable = $true + } + } +} diff --git a/src/State/Invoke-BuidState.ps1 b/src/State/Invoke-BuidState.ps1 new file mode 100644 index 0000000..2206216 --- /dev/null +++ b/src/State/Invoke-BuidState.ps1 @@ -0,0 +1,27 @@ +function Invoke-BuildState { + <# + .SYNOPSIS + Sets final Drone pipeline build state. + + .DESCRIPTION + Marks the pipeline ass succeeded of fail based on the custom state file. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] No pipeline output. + + .EXAMPLE + Invoke-BuildState + #> + [CmdletBinding()] + param() + + process { + $Repo = Get-RepoPath + if ( Test-Path -Path $Repo.FailureLogPath ) { + throw 'One one more pipeline steps failed. Marking the pipeline as failed!' + } + } +} diff --git a/src/State/Invoke-BuildState.Tests.ps1 b/src/State/Invoke-BuildState.Tests.ps1 new file mode 100644 index 0000000..a067314 --- /dev/null +++ b/src/State/Invoke-BuildState.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Invoke-BuildState' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Invoke-BuildState' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Invoke-BuildState' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Invoke-BuildState' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/State/Write-FailureStateFile.Tests.ps1 b/src/State/Write-FailureStateFile.Tests.ps1 new file mode 100644 index 0000000..ef6e36e --- /dev/null +++ b/src/State/Write-FailureStateFile.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Write-FailureStateFile' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Write-FailureStateFile' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Write-FailureStateFile' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Write-FailureStateFile' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/State/Write-FailureStateFile.ps1 b/src/State/Write-FailureStateFile.ps1 new file mode 100644 index 0000000..71a0676 --- /dev/null +++ b/src/State/Write-FailureStateFile.ps1 @@ -0,0 +1,48 @@ +function Write-FailureStateFile() { + <# + .SYNOPSIS + Writes the current pipeline step into failure log. + + .DESCRIPTION + This Cmdlet is used to mark single steps as failed without stopping the complete pipeline. + + .PARAMETER StepName + The current DroneHelper step name which should be added into to the log. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] No pipeline output. + + .EXAMPLE + Write-FailureStateFile + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseConsistentWhitespace', + '', + Justification = 'justification' + )] + param ( + [Parameter(Mandatory = $true)] + [string]$StepName + ) + + process { + $Repo = Get-RepoPath + $WriteParams = @{ + FilePath = $Repo.FailureLogPath + Encoding = 'utf8' + NoClobber = $true + Force = $true + InputObject = $StepName + } + + if ( Test-Path -Path $Repo.FailureLogPath ) { + $WriteParams.Append = $true + } + + Out-File @WriteParams + } +} diff --git a/src/State/Write-RestultFile.Tests.ps1 b/src/State/Write-RestultFile.Tests.ps1 new file mode 100644 index 0000000..381b60d --- /dev/null +++ b/src/State/Write-RestultFile.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force +} +Describe 'Write-ResultFile' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Write-ResultFile' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Write-ResultFile' } | Should -Not -Throw + } + It 'Help Content' { + $foo = Get-Help -Name 'Write-ResultFile' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } +} diff --git a/src/State/Write-ResultFile.ps1 b/src/State/Write-ResultFile.ps1 new file mode 100644 index 0000000..82123ba --- /dev/null +++ b/src/State/Write-ResultFile.ps1 @@ -0,0 +1,59 @@ +function Write-ResultFile { + <# + .SYNOPSIS + Writes the current pipeline step into failure log. + + .DESCRIPTION + This Cmdlet is used to mark single steps as failed without stopping the complete pipeline. + + .PARAMETER StepName + The current DroneHelper step name which should be added into to the log. + + .INPUTS + [None] No pipeline input. + + .OUTPUTS + [None] No pipeline output. + + .EXAMPLE + Write-FailureStateFile + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [PSCustomObject]$InputObject, + + [Parameter(Mandatory = $true)] + [String]$Path, + + [Parameter(Mandatory = $true)] + [ValidateSet('Pester', 'PSScriptAnalyzer', 'FileLinter', 'Custom')] + [String]$Type + ) + + process { + [String[]]$Output = @() + if ($BlockDescription -ne "") { + $BlockDescription | Out-File -FilePath $Path -Encoding utf8 -Force -NoClobber -Append + } + + switch ($Type) { + 'Pester' { + $Output = Format-PesterReport -InputObject $InputObject + } + + 'PSScriptAnalyzer' { + $Output = Format-ScriptAnalyzerReport -InputObject $InputObject + } + + 'FileLinter' { + $Output = Format-FileLinterReport -InputObject $InputObject + } + 'Custom' { + # nothing to do here + $Output = $InputObject + [Environment]::NewLine + } + } + $Output | Out-File -FilePath $Path -Encoding utf8 -Force -NoClobber -Append + } +}