From 260cdb4de028a2953a170cb717cf74dcdfbde7d2 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 17 Jan 2024 23:16:48 +1000 Subject: [PATCH] Update to build process and tool chain (#231) --- .azure-pipelines/azure-pipelines.yaml | 165 ----- .azure-pipelines/integration-pipeline.yaml | 36 - .../jobs/integrationContainer.yaml | 61 -- .azure-pipelines/jobs/test.yaml | 76 -- .azure-pipelines/jobs/testContainer.yaml | 76 -- .devcontainer/devcontainer.json | 58 +- .editorconfig | 39 +- .github/workflows/analyze.yaml | 89 ++- .github/workflows/build.yaml | 145 +++- .github/workflows/dependencies.yaml | 7 +- .github/workflows/first-interaction.yaml | 27 + .github/workflows/stale.yaml | 45 +- .ps-rule/Rule.Rule.ps1 | 4 +- .vscode/extensions.json | 13 +- .vscode/settings.json | 101 +-- .vscode/tasks.json | 164 ++--- CHANGELOG.md | 2 + README.md | 4 +- modules.json | 7 +- pipeline.build.ps1 | 26 +- ps-rule.yaml | 12 +- .../pipeline-deps.ps1 | 0 src/PSRule.Common.props | 3 +- .../Common/DictionaryExtensions.cs | 31 +- .../Common/HttpClientExtensions.cs | 43 +- .../Common/JsonConverters.cs | 347 +++++---- .../Common/PSObjectExtensions.cs | 11 +- .../Configuration/ConfigurationOption.cs | 83 ++- .../Configuration/OutputEncoding.cs | 21 +- .../Configuration/OutputOption.cs | 119 ++-- .../Configuration/PSRuleOption.cs | 173 +++-- src/PSRule.Rules.GitHub/Data/Models.cs | 343 +++++---- .../Pipeline/Exceptions.cs | 195 +++-- .../Pipeline/ExportPipeline.cs | 288 ++++---- .../Pipeline/GitHubClient.cs | 665 +++++++++--------- .../Pipeline/GitHubContext.cs | 91 ++- .../Pipeline/LoggingExtensions.cs | 15 +- .../Pipeline/Output/FileOutputWriter.cs | 75 +- .../Pipeline/Output/JsonOutputWriter.cs | 27 +- .../Pipeline/Output/PSPipelineWriter.cs | 327 +++++---- .../Pipeline/PipelineBuilder.cs | 289 ++++---- .../Pipeline/PipelineContext.cs | 17 +- .../Pipeline/PipelineWriter.cs | 323 +++++---- .../Pipeline/RepositoryHelper.cs | 75 +- src/PSRule.Rules.GitHub/Runtime/Helper.cs | 36 +- .../PSRule.Rules.GitHub.Tests.csproj | 4 +- 46 files changed, 2288 insertions(+), 2470 deletions(-) delete mode 100644 .azure-pipelines/azure-pipelines.yaml delete mode 100644 .azure-pipelines/integration-pipeline.yaml delete mode 100644 .azure-pipelines/jobs/integrationContainer.yaml delete mode 100644 .azure-pipelines/jobs/test.yaml delete mode 100644 .azure-pipelines/jobs/testContainer.yaml create mode 100644 .github/workflows/first-interaction.yaml rename {.azure-pipelines => scripts}/pipeline-deps.ps1 (100%) diff --git a/.azure-pipelines/azure-pipelines.yaml b/.azure-pipelines/azure-pipelines.yaml deleted file mode 100644 index b566eba..0000000 --- a/.azure-pipelines/azure-pipelines.yaml +++ /dev/null @@ -1,165 +0,0 @@ -# Azure DevOps -# CI pipeline for PSRule.Rules.GitHub - -variables: - version: '0.3.0' - buildConfiguration: 'Release' - disable.coverage.autogenerate: 'true' - imageName: 'ubuntu-20.04' - - # Use build number format, i.e. 0.3.0-B2202001 -name: $(version)-B$(date:yyMM)$(rev:rrr) - -trigger: - branches: - include: - - 'main' - tags: - include: - - 'v0.*' - -pr: - branches: - include: - - 'main' - -stages: - -# Build pipeline -- stage: Build - displayName: Build - dependsOn: [] - jobs: - - job: - pool: - vmImage: $(imageName) - displayName: 'Module' - steps: - - # Install pipeline dependencies - - powershell: ./.azure-pipelines/pipeline-deps.ps1 - displayName: 'Install dependencies' - - # Build module - - powershell: Invoke-Build -Configuration $(buildConfiguration) -Build $(Build.BuildNumber) - displayName: 'Build module' - - # DotNet test results - - task: PublishTestResults@2 - displayName: 'Publish unit test results' - inputs: - testRunTitle: 'DotNet on $(imageName)' - testRunner: VSTest - testResultsFiles: 'reports/*.trx' - mergeTestResults: true - platform: $(imageName) - configuration: $(buildConfiguration) - publishRunAttachments: true - condition: succeededOrFailed() - - # PSRule results - - task: PublishTestResults@2 - displayName: 'Publish PSRule results' - inputs: - testRunTitle: 'PSRule on $(imageName)' - testRunner: NUnit - testResultsFiles: 'reports/ps-rule*.xml' - mergeTestResults: true - platform: $(imageName) - configuration: $(buildConfiguration) - publishRunAttachments: true - condition: succeededOrFailed() - - # Generate artifacts - - publish: out/modules/PSRule.Rules.GitHub - displayName: 'Publish module' - artifact: PSRule.Rules.GitHub - -# Analysis pipeline -- stage: Analysis - displayName: Analysis - dependsOn: [] - variables: - skipComponentGovernanceDetection: 'true' - jobs: - - job: - pool: - vmImage: $(imageName) - displayName: 'SonarCloud' - condition: not(eq(variables['Build.Reason'], 'PullRequest')) - steps: - - - script: | - echo "##vso[task.setvariable variable=JAVA_HOME]$(JAVA_HOME_11_X64)" - echo "##vso[task.setvariable variable=PATH]$(JAVA_HOME_11_X64)\bin;$(PATH)" - displayName: 'Set Java version' - - # Run SonarCloud analysis - - script: dotnet tool install --global dotnet-sonarscanner - displayName: 'Install Sonar scanner' - - - script: $HOME/.dotnet/tools/dotnet-sonarscanner begin /k:"BernieWhite_PSRule_Rules_GitHub" /o:"berniewhite-github" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.login=$(sonarQubeToken) /v:"$(Build.BuildNumber)" /d:sonar.cs.vscoveragexml.reportsPaths="reports/" /d:sonar.cs.xunit.reportsPaths="reports/" - displayName: 'Prepare SonarCloud' - - - script: dotnet build - displayName: 'Build solution for analysis' - - - script: $HOME/.dotnet/tools/dotnet-sonarscanner end /d:sonar.login=$(sonarQubeToken) - displayName: 'Complete SonarCloud' - - - job: Secret_Scan - pool: - vmImage: 'windows-2019' - displayName: Secret scan - - steps: - - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2 - displayName: 'Scan for secrets' - inputs: - debugMode: false - toolMajorVersion: V2 - - - task: securedevelopmentteam.vss-secure-development-tools.build-task-publishsecurityanalysislogs.PublishSecurityAnalysisLogs@2 - displayName: 'Publish scan logs' - continueOnError: true - - - task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@1 - displayName: 'Check for failures' - inputs: - CredScan: true - ToolLogsNotFoundAction: Error - -# Test pipeline -- stage: Test - dependsOn: Build - variables: - skipComponentGovernanceDetection: 'true' - jobs: - - - template: jobs/test.yaml - parameters: - name: ubuntu_22_04_coverage - imageName: 'ubuntu-22.04' - displayName: 'PowerShell coverage' - coverage: 'true' - publishResults: 'false' - - - template: jobs/test.yaml - parameters: - name: macOS_11 - displayName: 'PowerShell 7.1 - macOS-11' - imageName: 'macOS-11' - - - template: jobs/test.yaml - parameters: - name: windows_2022 - displayName: 'PowerShell 7.1 - Windows 2022' - imageName: 'windows-2022' - pwsh: 'false' - - - template: jobs/testContainer.yaml - parameters: - name: ps_7_3_ubuntu_22_04 - displayName: 'PowerShell 7.3 - ubuntu-22.04' - imageName: mcr.microsoft.com/powershell - imageTag: 7.3-ubuntu-22.04 diff --git a/.azure-pipelines/integration-pipeline.yaml b/.azure-pipelines/integration-pipeline.yaml deleted file mode 100644 index 67a108b..0000000 --- a/.azure-pipelines/integration-pipeline.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Azure DevOps -# Pipeline to run integration tests for PSRule.Rules.GitHub - -variables: - buildConfiguration: 'Release' - imageName: 'ubuntu-20.04' - -resources: - pipelines: - - pipeline: CI - source: 'PSRule.Rules.GitHub-CI' - branch: main - trigger: - branches: - include: - - main - -# Use build number format, i.e. I2007009 -name: I$(date:yyMM)$(rev:rrr) - -trigger: none -pr: none - -stages: - -# Test pipeline -- stage: Test - variables: - skipComponentGovernanceDetection: 'true' - jobs: - - template: jobs/integrationContainer.yaml - parameters: - name: gh_integration - displayName: 'GitHub Integration Tests' - imageName: mcr.microsoft.com/powershell - imageTag: 7.2.2-ubuntu-20.04 diff --git a/.azure-pipelines/jobs/integrationContainer.yaml b/.azure-pipelines/jobs/integrationContainer.yaml deleted file mode 100644 index fffe500..0000000 --- a/.azure-pipelines/jobs/integrationContainer.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# Azure DevOps -# Job for running integration tests pipelines in a container - -parameters: - name: '' - displayName: '' - buildConfiguration: 'Release' - vmImage: 'ubuntu-22.04' - imageName: '' - imageTag: '' - publishResults: 'true' - -jobs: -- job: ${{ parameters.name }} - displayName: ${{ parameters.displayName }} - condition: not(eq(variables['Build.Reason'], 'PullRequest')) - pool: - vmImage: ${{ parameters.vmImage }} - container: - image: '${{ parameters.imageName }}:${{ parameters.imageTag }}' - env: - PUBLISHRESULTS: ${{ parameters.publishResults }} - variables: - PUBLISHRESULTS: ${{ parameters.publishResults }} - skipComponentGovernanceDetection: true - steps: - - # Install pipeline dependencies - - powershell: ./.azure-pipelines/pipeline-deps.ps1 - displayName: 'Install dependencies' - - # Download module - - task: DownloadPipelineArtifact@2 - displayName: 'Download module' - inputs: - artifact: PSRule.Rules.GitHub - source: 'specific' - runVersion: 'specific' - project: 'PSRule.Rules.GitHub' - pipeline: $(resources.pipeline.CI.pipelineID) - runId: $(resources.pipeline.CI.runID) - path: $(Build.SourcesDirectory)/out/modules/PSRule.Rules.GitHub - - # Build module - - powershell: Invoke-Build IntegrationTest -Configuration ${{ parameters.buildConfiguration }} -Build $(Build.BuildNumber) - displayName: 'Test module' - env: - GITHUB_TOKEN: $(GITHUB_TOKEN) - - # Pester test results - - task: PublishTestResults@2 - displayName: 'Publish Pester results' - inputs: - testRunTitle: 'Pester on ${{ parameters.imageTag }}' - testRunner: NUnit - testResultsFiles: 'reports/pester-unit.xml' - mergeTestResults: true - platform: ${{ parameters.imageTag }} - configuration: ${{ parameters.buildConfiguration }} - publishRunAttachments: true - condition: and(succeededOrFailed(), eq(variables['PUBLISHRESULTS'], 'true')) diff --git a/.azure-pipelines/jobs/test.yaml b/.azure-pipelines/jobs/test.yaml deleted file mode 100644 index 517d475..0000000 --- a/.azure-pipelines/jobs/test.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# Azure DevOps -# CI job for running VM pipelines - -parameters: - name: '' - displayName: '' - buildConfiguration: 'Release' - imageName: '' - coverage: 'false' - publishResults: 'true' - pwsh: 'true' - -jobs: -- job: ${{ parameters.name }} - displayName: ${{ parameters.displayName }} - pool: - vmImage: ${{ parameters.imageName }} - variables: - COVERAGE: ${{ parameters.coverage }} - PUBLISHRESULTS: ${{ parameters.publishResults }} - skipComponentGovernanceDetection: true - steps: - - # Install pipeline dependencies - - powershell: ./.azure-pipelines/pipeline-deps.ps1 - displayName: 'Install dependencies' - - # Download module - - task: DownloadPipelineArtifact@2 - displayName: 'Download module' - inputs: - artifact: PSRule.Rules.GitHub - path: $(Build.SourcesDirectory)/out/modules/PSRule.Rules.GitHub - - # Build module - - task: PowerShell@2 - inputs: - targetType: inline - script: Invoke-Build TestModule -Configuration ${{ parameters.buildConfiguration }} -Build $(Build.BuildNumber) - pwsh: ${{ eq(parameters.pwsh, 'true') }} - env: - COVERAGE: ${{ parameters.coverage }} - displayName: 'Test module' - - # Pester test results - - task: PublishTestResults@2 - displayName: 'Publish Pester results' - inputs: - testRunTitle: 'Pester on ${{ parameters.imageName }}' - testRunner: NUnit - testResultsFiles: 'reports/pester-unit.xml' - mergeTestResults: true - platform: ${{ parameters.name }} - configuration: ${{ parameters.buildConfiguration }} - publishRunAttachments: true - condition: and(succeededOrFailed(), eq(variables['PUBLISHRESULTS'], 'true')) - - # Generate Code Coverage report - - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 - displayName: 'Code coverage report generator' - inputs: - reports: 'reports/pester-coverage.xml' - targetdir: 'reports/coverage' - sourcedirs: 'src/PSRule.Rules.GitHub' - reporttypes: 'HtmlInline_AzurePipelines;Cobertura;SonarQube;Badges' - tag: $(Build.BuildNumber) - condition: eq(variables['COVERAGE'], 'true') - - # Publish Code Coverage report - - task: PublishCodeCoverageResults@1 - displayName: 'Publish Pester code coverage' - inputs: - codeCoverageTool: 'Cobertura' - summaryFileLocation: 'reports/coverage/Cobertura.xml' - reportDirectory: 'reports/coverage' - condition: eq(variables['COVERAGE'], 'true') diff --git a/.azure-pipelines/jobs/testContainer.yaml b/.azure-pipelines/jobs/testContainer.yaml deleted file mode 100644 index babe487..0000000 --- a/.azure-pipelines/jobs/testContainer.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# Azure DevOps -# CI job for running container pipelines - -parameters: - name: '' - displayName: '' - buildConfiguration: 'Release' - vmImage: 'ubuntu-22.04' - imageName: '' - imageTag: '' - coverage: 'false' - publishResults: 'true' - -jobs: -- job: ${{ parameters.name }} - displayName: ${{ parameters.displayName }} - pool: - vmImage: ${{ parameters.vmImage }} - container: - image: '${{ parameters.imageName }}:${{ parameters.imageTag }}' - env: - COVERAGE: ${{ parameters.coverage }} - PUBLISHRESULTS: ${{ parameters.publishResults }} - variables: - COVERAGE: ${{ parameters.coverage }} - PUBLISHRESULTS: ${{ parameters.publishResults }} - skipComponentGovernanceDetection: true - steps: - - # Install pipeline dependencies - - powershell: ./.azure-pipelines/pipeline-deps.ps1 - displayName: 'Install dependencies' - - # Download module - - task: DownloadPipelineArtifact@2 - displayName: 'Download module' - inputs: - artifact: PSRule.Rules.GitHub - path: $(Build.SourcesDirectory)/out/modules/PSRule.Rules.GitHub - - # Build module - - powershell: Invoke-Build TestModule -Configuration ${{ parameters.buildConfiguration }} -Build $(Build.BuildNumber) - displayName: 'Test module' - - # Pester test results - - task: PublishTestResults@2 - displayName: 'Publish Pester results' - inputs: - testRunTitle: 'Pester on ${{ parameters.imageTag }}' - testRunner: NUnit - testResultsFiles: 'reports/pester-unit.xml' - mergeTestResults: true - platform: ${{ parameters.imageTag }} - configuration: ${{ parameters.buildConfiguration }} - publishRunAttachments: true - condition: and(succeededOrFailed(), eq(variables['PUBLISHRESULTS'], 'true')) - - # Generate Code Coverage report - - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 - displayName: 'Code coverage report generator' - inputs: - reports: 'reports\pester-coverage.xml' - targetdir: 'reports\coverage' - sourcedirs: 'src\PSRule.Rules.GitHub' - reporttypes: 'HtmlInline_AzurePipelines;Cobertura;SonarQube;Badges' - tag: $(Build.BuildNumber) - condition: eq(variables['COVERAGE'], 'true') - - # Publish Code Coverage report - - task: PublishCodeCoverageResults@1 - displayName: 'Publish Pester code coverage' - inputs: - codeCoverageTool: 'Cobertura' - summaryFileLocation: 'reports/coverage/Cobertura.xml' - reportDirectory: 'reports/coverage' - condition: eq(variables['COVERAGE'], 'true') diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5cf92c8..b41c310 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,27 +1,47 @@ { - "name": "PSRule for GitHub dev", - "settings": { + "name": "PSRule for GitHub Developer Codespace", + "customizations": { + "vscode": { + "settings": { "terminal.integrated.defaultProfile.linux": "pwsh", "terminal.integrated.profiles.linux": { - "pwsh": { - "path": "/bin/pwsh" - } - } - }, - "extensions": [ - "ms-azure-devops.azure-pipelines", - "davidanson.vscode-markdownlint", + "pwsh": { + "path": "/opt/microsoft/powershell/7/pwsh" + } + }, + "powershell.powerShellDefaultVersion": "PowerShell" + }, + "extensions": [ + "ms-dotnettools.vscode-dotnet-runtime", + "ms-dotnettools.csdevkit", + "ms-vscode.powershell", + "GitHub.vscode-pull-request-github", + "github.vscode-github-actions", "bewhite.psrule-vscode-preview", - "msazurermtools.azurerm-vscode-tools", - "ms-azuretools.vscode-bicep", - "ms-dotnettools.csharp", + "davidanson.vscode-markdownlint", "eamodio.gitlens", - "github.vscode-pull-request-github", "streetsidesoftware.code-spell-checker" - ], - "features": { - "github-cli": "latest" + ] + } + }, + "features": { + "ghcr.io/devcontainers/features/github-cli": { + "version": "latest" }, - "onCreateCommand": "/bin/pwsh -f .devcontainer/container-build.ps1", - "postStartCommand": "/bin/pwsh -f .devcontainer/container-start.ps1" + "ghcr.io/devcontainers/features/powershell": { + "version": "latest" + } + }, + "onCreateCommand": "/opt/microsoft/powershell/7/pwsh -f .devcontainer/container-build.ps1", + "postStartCommand": "/opt/microsoft/powershell/7/pwsh -f .devcontainer/container-start.ps1", + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "7.0-bullseye-slim" + } + }, + "remoteUser": "vscode", + "forwardPorts": [ + 8000 + ] } diff --git a/.editorconfig b/.editorconfig index 2ca0816..a06e4ea 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,7 @@ root = true [*] charset = utf-8 indent_style = space +insert_final_newline = true # Source code [*.{cs,ps1,psd1,psm1}] @@ -20,12 +21,15 @@ indent_size = 2 [*.cs] # Code style defaults -csharp_using_directive_placement = outside_namespace:suggestion +csharp_using_directive_placement = outside_namespace:error +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion dotnet_sort_system_directives_first = true dotnet_style_readonly_field = true:suggestion +csharp_style_namespace_declarations = file_scoped:error # License header file_header_template = Copyright (c) Microsoft Corporation.\nLicensed under the MIT License. +dotnet_diagnostic.IDE0073.severity = error # Suggest more modern language features when available dotnet_style_object_initializer = true:suggestion @@ -41,7 +45,38 @@ dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion dotnet_style_prefer_conditional_expression_over_return = true:suggestion csharp_prefer_simple_default_expression = true:suggestion +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_switch_expression = false:none +csharp_style_prefer_pattern_matching = false:none + # Prefer "var" everywhere csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_var_elsewhere = true:suggestion \ No newline at end of file +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Define the 'private_fields' symbol group: +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +# Define the 'private_static_fields' symbol group +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +# Define the 'underscored' naming style +dotnet_naming_style.underscored.capitalization = pascal_case +dotnet_naming_style.underscored.required_prefix = _ + +# Private instance fields must use pascal case with a leading '_' +dotnet_naming_rule.private_fields_underscored.symbols = private_fields +dotnet_naming_rule.private_fields_underscored.style = underscored +dotnet_naming_rule.private_fields_underscored.severity = error + +# Exclude private static fields from underscored style +dotnet_naming_rule.private_static_fields_none.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_none.style = underscored +dotnet_naming_rule.private_static_fields_none.severity = none diff --git a/.github/workflows/analyze.yaml b/.github/workflows/analyze.yaml index b0756c9..3f2bc6f 100644 --- a/.github/workflows/analyze.yaml +++ b/.github/workflows/analyze.yaml @@ -12,29 +12,52 @@ name: Analyze on: push: - branches: [ main, 'release/*' ] + branches: [main, 'release/*'] pull_request: - branches: [ main, 'release/*' ] + branches: [main, 'release/*'] schedule: - - cron: '42 20 * * 0' # At 08:42 PM, on Sunday each week + - cron: '24 22 * * 0' # At 10:24 PM, on Sunday each week workflow_dispatch: +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +permissions: {} + jobs: oss: name: Analyze with PSRule runs-on: ubuntu-latest permissions: contents: read + security-events: write steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Checkout - uses: actions/checkout@v3 + - name: Run PSRule analysis + uses: microsoft/ps-rule@v2.9.0 + with: + modules: PSRule.Rules.MSFT.OSS + prerelease: true + outputFormat: Sarif + outputPath: reports/ps-rule-results.sarif - - name: Run PSRule analysis - uses: microsoft/ps-rule@v2.9.0 - with: - modules: PSRule.Rules.MSFT.OSS - prerelease: true + - name: Upload results to security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: reports/ps-rule-results.sarif + + - name: Upload results + uses: actions/upload-artifact@v4 + if: always() + with: + name: PSRule-Sarif + path: reports/ps-rule-results.sarif + retention-days: 1 + if-no-files-found: error devskim: name: Analyze with DevSkim @@ -44,20 +67,29 @@ jobs: contents: read security-events: write steps: - - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run DevSkim scanner uses: microsoft/DevSkim-Action@v1 with: - directory-to-scan: src/ + directory-to-scan: . - name: Upload results to security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 + if: always() with: sarif_file: devskim-results.sarif + - name: Upload results + uses: actions/upload-artifact@v4 + if: always() + with: + name: DevSkim-Sarif + path: devskim-results.sarif + retention-days: 1 + if-no-files-found: error + codeql: name: Analyze with CodeQL runs-on: ubuntu-latest @@ -66,17 +98,26 @@ jobs: contents: read security-events: write steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Checkout - uses: actions/checkout@v3 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: 'csharp' - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: 'csharp' + - name: Autobuild + uses: github/codeql-action/autobuild@v3 - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + id: codeql-analyze - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Upload results + uses: actions/upload-artifact@v4 + if: always() + with: + name: CodeQL-Sarif + path: ${{ steps.codeql-analyze.outputs.sarif-output }} + retention-days: 1 + if-no-files-found: error diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3b000f2..b04748e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,30 +1,143 @@ -# CI workflow for validating action +# +# CI Pipeline +# + +# NOTES: +# This workflow builds and tests module updates. + name: Build on: push: - branches: [ main, 'release/*' ] + branches: [main, 'release/*'] pull_request: - branches: [ main, 'release/*' ] + branches: [main, 'release/*'] + workflow_dispatch: + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +permissions: {} jobs: - - test: - name: Test + build: + name: Build runs-on: ubuntu-latest permissions: actions: read checks: read contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Checkout - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 7.x - - name: Run PSRule self analysis - shell: pwsh - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - . ./.azure-pipelines/pipeline-deps.ps1 - Invoke-Build Build -AssertStyle GitHubActions - Invoke-Build IntegrationTest -AssertStyle GitHubActions + - name: Install dependencies + shell: pwsh + timeout-minutes: 3 + run: ./scripts/pipeline-deps.ps1 + + - name: Build module + shell: pwsh + timeout-minutes: 5 + run: Invoke-Build -Configuration Release -AssertStyle GitHubActions + + - name: Run PSRule self analysis + shell: pwsh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + Invoke-Build IntegrationTest -AssertStyle GitHubActions + + - name: Upload module + uses: actions/upload-artifact@v4 + with: + name: Module + path: ./out/modules/PSRule.Rules.GitHub/* + retention-days: 3 + if-no-files-found: error + + - name: Upload PSRule Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: Results-PSRule + path: ./reports/ps-rule*.xml + retention-days: 3 + if-no-files-found: error + + test: + name: Test (${{ matrix.rid }}-${{ matrix.shell }}) + runs-on: ${{ matrix.os }} + needs: build + permissions: + contents: read + + strategy: + # Get full test results from all platforms. + fail-fast: false + + matrix: + os: ['ubuntu-latest'] + rid: ['linux-x64'] + shell: ['pwsh'] + include: + - os: windows-latest + rid: win-x64 + shell: pwsh + - os: windows-latest + rid: win-x64 + shell: powershell + - os: ubuntu-latest + rid: linux-x64 + shell: pwsh + - os: ubuntu-latest + rid: linux-musl-x64 + shell: pwsh + - os: macos-latest + rid: osx-x64 + shell: pwsh + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 7.x + + - if: ${{ matrix.shell == 'pwsh' }} + name: Install dependencies (PowerShell) + shell: pwsh + timeout-minutes: 3 + run: ./scripts/pipeline-deps.ps1 + + - if: ${{ matrix.shell == 'powershell' }} + name: Install dependencies (Windows PowerShell) + shell: powershell + timeout-minutes: 3 + run: ./scripts/pipeline-deps.ps1 + + - name: Download module + uses: actions/download-artifact@v4 + with: + name: Module + path: ./out/modules/PSRule.Rules.GitHub + + - if: ${{ matrix.shell == 'pwsh' }} + name: Test module (PowerShell) + shell: pwsh + timeout-minutes: 15 + run: Invoke-Build TestModule -Configuration Release -AssertStyle GitHubActions + + - if: ${{ matrix.shell == 'powershell' }} + name: Test module (Windows PowerShell) + shell: powershell + timeout-minutes: 30 + run: Invoke-Build TestModule -Configuration Release -AssertStyle GitHubActions diff --git a/.github/workflows/dependencies.yaml b/.github/workflows/dependencies.yaml index 0b4505c..93ad7b5 100644 --- a/.github/workflows/dependencies.yaml +++ b/.github/workflows/dependencies.yaml @@ -8,12 +8,14 @@ name: Dependencies on: schedule: - - cron: '30 1 * * 1' # At 01:30 AM, on Monday each week + - cron: '30 1 * * 1' # At 01:30 AM, on Monday each week workflow_dispatch: env: WORKING_BRANCH: dependencies/powershell-bump +permissions: {} + jobs: dependencies: name: Bump dependencies @@ -23,9 +25,8 @@ jobs: contents: write pull-requests: write steps: - - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/first-interaction.yaml b/.github/workflows/first-interaction.yaml new file mode 100644 index 0000000..a1389fd --- /dev/null +++ b/.github/workflows/first-interaction.yaml @@ -0,0 +1,27 @@ +# +# Stale item management +# + +# NOTES: +# This workflow greets a person for their a first issue or PR. + +name: First interaction + +on: [pull_request_target, issues] + +permissions: {} + +jobs: + greeting: + name: Greeting + runs-on: ubuntu-latest + if: github.repository == 'microsoft/PSRule.Rules.GitHub' + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: 'Thanks for raising your first issue, the team appreciates the time you have taken 😉' + pr-message: 'Thank you for your contribution, one of the team will evaluate shortly.' diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index e578942..d210141 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -1,39 +1,40 @@ # -# Stale issues +# Stale item management # # NOTES: -# Repository stale issue management. -# Issues with open ended labels are automatically closed if no activity occurs. -# Issues are marked stale after 14 days, then closed after a further 7 days. +# This workflow manages stale work items on the repository. -name: 'Close stale issues' +name: Stale maintenance on: schedule: - - cron: '30 1 * * *' # At 1:30 AM, daily + - cron: '30 1 * * *' + workflow_dispatch: + +permissions: {} jobs: - stale: + issue: + name: Close stale issues runs-on: ubuntu-latest + if: github.repository == 'microsoft/PSRule.Rules.GitHub' permissions: issues: write - pull-requests: write steps: + - uses: actions/stale@v9 + with: + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs within 7 days. + Thank you for your contributions. - - uses: actions/stale@v8 - with: - stale-issue-message: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs within 7 days. - Thank you for your contributions. + close-issue-message: 'This issue was closed because it has not had any recent activity.' - close-issue-message: 'This issue was closed because it has not had any recent activity.' + days-before-stale: 14 + days-before-pr-stale: -1 - days-before-stale: 14 - days-before-pr-stale: -1 + days-before-close: 7 + days-before-pr-close: -1 - days-before-close: 7 - days-before-pr-close: -1 - - any-of-labels: 'question,duplicate,incomplete,waiting-feedback' - stale-issue-label: stale + any-of-labels: 'question,duplicate,incomplete,waiting-feedback' + stale-issue-label: stale diff --git a/.ps-rule/Rule.Rule.ps1 b/.ps-rule/Rule.Rule.ps1 index ebf87e9..a7c72ac 100644 --- a/.ps-rule/Rule.Rule.ps1 +++ b/.ps-rule/Rule.Rule.ps1 @@ -5,8 +5,8 @@ Rule 'Rule.Name' -Type 'PSRule.Rules.Rule' { Recommend 'Rule name should be less than 35 characters to prevent being truncated.' Reason 'The rule name is too long.' - $Assert.LessOrEqual($TargetObject, 'RuleName', 35) - $Assert.StartsWith($TargetObject, 'RuleName', "$($Configuration.RULE_AUTHORING_PREFIX).") + $Assert.LessOrEqual($PSRule, 'TargetName', 35) + $Assert.StartsWith($PSRule, 'TargetName', "$($Configuration.RULE_AUTHORING_PREFIX).") } # Synopsis: Complete help documentation diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 05f9320..f380087 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,9 +1,8 @@ { - "recommendations": [ - "ms-vscode.powershell", - "ms-azure-devops.azure-pipelines", - "redhat.vscode-yaml", - "bewhite.psrule-vscode-preview", - "streetsidesoftware.code-spell-checker" - ] + "recommendations": [ + "ms-vscode.powershell", + "redhat.vscode-yaml", + "bewhite.psrule-vscode-preview", + "streetsidesoftware.code-spell-checker" + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b436bc..f2ce097 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,48 +1,59 @@ { - "files.exclude": { - "reports/": true, - "out/": true, - ".vs/": true, - "**/bin/": true, - "**/obj/": true - }, - "search.exclude": { - "out/": true - }, - "editor.insertSpaces": true, + "files.exclude": { + "reports/": true, + "out/": true, + ".vs/": true, + "**/bin/": true, + "**/obj/": true, + ".venv/": true + }, + "search.exclude": { + "out/": true + }, + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.formatOnSave": true, + "editor.detectIndentation": false, + "files.insertFinalNewline": true, + "yaml.format.singleQuote": true, + "[markdown]": { + "editor.formatOnSave": false + }, + "[csharp]": { "editor.tabSize": 4, - "[yaml]": { - "editor.tabSize": 2 - }, - "[markdown]": { - "editor.tabSize": 2 - }, - "files.associations": { - "**/.azure-pipelines/*.yaml": "azure-pipelines", - "**/.azure-pipelines/jobs/*.yaml": "azure-pipelines" - }, - "cSpell.words": [ - "Octokit", - "cmdlets" - ], - "cSpell.enabledLanguageIds": [ - "csharp", - "git-commit", - "markdown", - "plaintext", - "powershell", - "text", - "yaml", - "yml" - ], - "[csharp]": { - "editor.formatOnSave": true - }, - "omnisharp.enableImportCompletion": true, - "omnisharp.organizeImportsOnFormat": true, - "omnisharp.enableEditorConfigSupport": true, - "omnisharp.enableRoslynAnalyzers": true, - "git.branchProtection": [ - "main" - ] + }, + "[yaml]": { + "editor.formatOnSave": false + }, + "[powershell]": { + "editor.tabSize": 4, + "editor.formatOnSave": false + }, + "[html]": { + "editor.formatOnSave": false + }, + "files.associations": { + "**/CODEOWNERS": "text" + }, + "cSpell.words": [ + "Octokit", + "cmdlets" + ], + "cSpell.enabledLanguageIds": [ + "csharp", + "git-commit", + "markdown", + "plaintext", + "powershell", + "text", + "yaml", + "yml" + ], + "omnisharp.organizeImportsOnFormat": true, + "omnisharp.enableEditorConfigSupport": true, + "git.branchProtection": [ + "main", + "release/*" + ], + "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8cb8f0b..b7de8bf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,84 +1,84 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "test", - "type": "shell", - "detail": "Build and test module.", - "command": "Invoke-Build Test -AssertStyle Client", - "group": { - "kind": "test", - "isDefault": true - }, - "problemMatcher": [ - "$pester" - ], - "presentation": { - "clear": true, - "panel": "dedicated" - } - }, - { - "label": "coverage", - "type": "shell", - "command": "Invoke-Build Test -CodeCoverage", - "problemMatcher": [ - "$pester" - ], - "presentation": { - "clear": true, - "panel": "dedicated" - } - }, - { - "label": "build", - "detail": "Build module.", - "type": "shell", - "command": "Invoke-Build Build", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": [], - "presentation": { - "clear": true, - "panel": "dedicated" - } - }, - { - "label": "clean", - "type": "shell", - "command": "Invoke-Build Clean", - "problemMatcher": [] - }, - { - "label": "script-analyzer", - "type": "shell", - "command": "Invoke-Build Analyze", - "problemMatcher": [], - "presentation": { - "clear": true, - "panel": "dedicated" - } - }, - { - "label": "build-docs", - "type": "shell", - "command": "Invoke-Build BuildHelp", - "problemMatcher": [] - }, - { - "label": "scaffold-docs", - "type": "shell", - "command": "Invoke-Build ScaffoldHelp", - "problemMatcher": [] - }, - { - "label": "Rule docs", - "detail": "Generate rule table of contents.", - "type": "shell", - "command": "Invoke-Build BuildRuleDocs", - "problemMatcher": [] - } - ] + "version": "2.0.0", + "tasks": [ + { + "label": "test", + "type": "shell", + "detail": "Build and test module.", + "command": "Invoke-Build Test -AssertStyle Client", + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [ + "$pester" + ], + "presentation": { + "clear": true, + "panel": "dedicated" + } + }, + { + "label": "coverage", + "type": "shell", + "command": "Invoke-Build Test -CodeCoverage", + "problemMatcher": [ + "$pester" + ], + "presentation": { + "clear": true, + "panel": "dedicated" + } + }, + { + "label": "build", + "detail": "Build module.", + "type": "shell", + "command": "Invoke-Build Build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + } + }, + { + "label": "clean", + "type": "shell", + "command": "Invoke-Build Clean", + "problemMatcher": [] + }, + { + "label": "script-analyzer", + "type": "shell", + "command": "Invoke-Build Analyze", + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + } + }, + { + "label": "build-docs", + "type": "shell", + "command": "Invoke-Build BuildHelp", + "problemMatcher": [] + }, + { + "label": "scaffold-docs", + "type": "shell", + "command": "Invoke-Build ScaffoldHelp", + "problemMatcher": [] + }, + { + "label": "Rule docs", + "detail": "Generate rule table of contents.", + "type": "shell", + "command": "Invoke-Build BuildRuleDocs", + "problemMatcher": [] + } + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dd9bb6..f9cb552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ What's changed since pre-release v0.4.0-B0016: - Engineering: + - Bump PSRule to v2.9.0. + [#231](https://github.com/microsoft/PSRule.Rules.GitHub/pull/231) - Bump Octokit to v6.0.0. [#202](https://github.com/microsoft/PSRule.Rules.GitHub/pull/202) - Bump Microsoft.NET.Test.Sdk to v17.6.1. diff --git a/README.md b/README.md index 0b8db32..c8d9180 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 # STEP 2: Run analysis against exported data - name: Analyze repository - uses: microsoft/ps-rule@v2.2.0 + uses: microsoft/ps-rule@v2.9.0 with: modules: 'PSRule.Rules.GitHub' ``` diff --git a/modules.json b/modules.json index 74ab671..9252ad0 100644 --- a/modules.json +++ b/modules.json @@ -1,12 +1,12 @@ { "dependencies": { "PSRule": { - "version": "2.7.0" + "version": "2.9.0" } }, "devDependencies": { "Pester": { - "version": "5.4.0" + "version": "5.4.1" }, "platyPS": { "version": "0.14.2" @@ -16,6 +16,9 @@ }, "PSScriptAnalyzer": { "version": "1.21.0" + }, + "PSRule.Rules.MSFT.OSS": { + "version": "1.1.0" } } } diff --git a/pipeline.build.ps1 b/pipeline.build.ps1 index 63679ca..5ddeea6 100644 --- a/pipeline.build.ps1 +++ b/pipeline.build.ps1 @@ -159,18 +159,7 @@ task BuildDotNet { } task TestDotNet { - if ($CodeCoverage) { - exec { - # Test library - dotnet test --collect:"Code Coverage" --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Rules.GitHub.Tests - } - } - else { - exec { - # Test library - dotnet test --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Rules.GitHub.Tests - } - } + dotnet test } task CopyModule { @@ -278,17 +267,18 @@ task IntegrationTest ModuleDependencies, { # Synopsis: Run validation task Rules Dependencies, { $assertParams = @{ - Path = './.ps-rule/' - Style = $AssertStyle + Path = './.ps-rule/' + Style = $AssertStyle OutputFormat = 'NUnit3' - ErrorAction = 'Stop' - As = 'Summary' + ErrorAction = 'Stop' + As = 'Summary' + Outcome = 'Problem' } Import-Module (Join-Path -Path $PWD -ChildPath out/modules/PSRule.Rules.GitHub) -Force; - Assert-PSRule @assertParams -InputPath $PWD -Module PSRule.Rules.MSFT.OSS -Format File -OutputPath reports/ps-rule-file.xml; + Assert-PSRule @assertParams -InputPath $PWD -Module PSRule.Rules.MSFT.OSS -Format File -OutputPath ./reports/ps-rule-file.xml; $rules = Get-PSRule -Module PSRule.Rules.GitHub; - $rules | Assert-PSRule @assertParams -OutputPath reports/ps-rule-file2.xml; + $rules | Assert-PSRule @assertParams -OutputPath ./reports/ps-rule-file2.xml; } # Synopsis: Run script analyzer diff --git a/ps-rule.yaml b/ps-rule.yaml index a0aa098..701e7fe 100644 --- a/ps-rule.yaml +++ b/ps-rule.yaml @@ -6,11 +6,13 @@ # https://microsoft.github.io/PSRule/ requires: - PSRule: '@pre >=2.2.0' + PSRule: '@pre >=2.9.0' input: pathIgnore: - '.vscode/' + - '.github/workflows/' + - 'docs/examples*.json' - '*.md' - '*.Designer.cs' - '*.resx' @@ -18,6 +20,14 @@ input: - '*.txt' - '*.html' - '*.ico' + - '*.png' + - 'ps-docs.yaml' + - 'ps-project.yaml' + - 'ps-rule.yaml' + - 'mkdocs.yml' + - '**/.editorconfig' + - '.markdownlint.json' + - '.github/dependabot.yml' include: path: [] diff --git a/.azure-pipelines/pipeline-deps.ps1 b/scripts/pipeline-deps.ps1 similarity index 100% rename from .azure-pipelines/pipeline-deps.ps1 rename to scripts/pipeline-deps.ps1 diff --git a/src/PSRule.Common.props b/src/PSRule.Common.props index 6c93413..3be8db4 100644 --- a/src/PSRule.Common.props +++ b/src/PSRule.Common.props @@ -3,7 +3,8 @@ netstandard2.0 - 9.0 + 12.0 + en-US true portable diff --git a/src/PSRule.Rules.GitHub/Common/DictionaryExtensions.cs b/src/PSRule.Rules.GitHub/Common/DictionaryExtensions.cs index ae32987..13d8e9b 100644 --- a/src/PSRule.Rules.GitHub/Common/DictionaryExtensions.cs +++ b/src/PSRule.Rules.GitHub/Common/DictionaryExtensions.cs @@ -4,22 +4,21 @@ using System.Collections.Generic; using System.Diagnostics; -namespace PSRule.Rules.GitHub -{ - internal static class DictionaryExtensions - { - [DebuggerStepThrough] - public static bool TryPopValue(this IDictionary dictionary, string key, out object value) - { - return dictionary.TryGetValue(key, out value) && dictionary.Remove(key); - } +namespace PSRule.Rules.GitHub; - [DebuggerStepThrough] - public static void AddUnique(this IDictionary dictionary, IEnumerable> values) - { - foreach (var kv in values) - if (!dictionary.ContainsKey(kv.Key)) - dictionary.Add(kv.Key, kv.Value); - } +internal static class DictionaryExtensions +{ + [DebuggerStepThrough] + public static bool TryPopValue(this IDictionary dictionary, string key, out object value) + { + return dictionary.TryGetValue(key, out value) && dictionary.Remove(key); + } + + [DebuggerStepThrough] + public static void AddUnique(this IDictionary dictionary, IEnumerable> values) + { + foreach (var kv in values) + if (!dictionary.ContainsKey(kv.Key)) + dictionary.Add(kv.Key, kv.Value); } } diff --git a/src/PSRule.Rules.GitHub/Common/HttpClientExtensions.cs b/src/PSRule.Rules.GitHub/Common/HttpClientExtensions.cs index 22e047d..8eba78a 100644 --- a/src/PSRule.Rules.GitHub/Common/HttpClientExtensions.cs +++ b/src/PSRule.Rules.GitHub/Common/HttpClientExtensions.cs @@ -4,28 +4,27 @@ using System.Net.Http; using Newtonsoft.Json; -namespace PSRule.Rules.GitHub -{ - internal static class HttpClientExtensions - { - public static T Get(this HttpClient client, string requestUri, string[] headers) - { - var message = new HttpRequestMessage(HttpMethod.Get, requestUri); - for (var i = 0; i < headers.Length; i++) - message.Headers.Accept.ParseAdd(headers[i]); +namespace PSRule.Rules.GitHub; - var requestTask = client.SendAsync(message); - requestTask.Wait(); - var response = requestTask.Result; - response.EnsureSuccessStatusCode(); - var contentTask = response.Content.ReadAsStringAsync(); - contentTask.Wait(); - var settings = new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore - }; - settings.Converters.Add(new GitHubCommunityConverter()); - return JsonConvert.DeserializeObject(contentTask.Result, settings); - } +internal static class HttpClientExtensions +{ + public static T Get(this HttpClient client, string requestUri, string[] headers) + { + var message = new HttpRequestMessage(HttpMethod.Get, requestUri); + for (var i = 0; i < headers.Length; i++) + message.Headers.Accept.ParseAdd(headers[i]); + + var requestTask = client.SendAsync(message); + requestTask.Wait(); + var response = requestTask.Result; + response.EnsureSuccessStatusCode(); + var contentTask = response.Content.ReadAsStringAsync(); + contentTask.Wait(); + var settings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }; + settings.Converters.Add(new GitHubCommunityConverter()); + return JsonConvert.DeserializeObject(contentTask.Result, settings); } } diff --git a/src/PSRule.Rules.GitHub/Common/JsonConverters.cs b/src/PSRule.Rules.GitHub/Common/JsonConverters.cs index 673a399..76aaf55 100644 --- a/src/PSRule.Rules.GitHub/Common/JsonConverters.cs +++ b/src/PSRule.Rules.GitHub/Common/JsonConverters.cs @@ -9,214 +9,213 @@ using Newtonsoft.Json; using PSRule.Rules.GitHub.Pipeline; using PSRule.Rules.GitHub.Resources; -namespace PSRule.Rules.GitHub +namespace PSRule.Rules.GitHub; + +/// +/// A custom serializer to correctly convert PSObject properties to JSON instead of CLIXML. +/// +internal sealed class PSObjectJsonConverter : JsonConverter { - /// - /// A custom serializer to correctly convert PSObject properties to JSON instead of CLIXML. - /// - internal sealed class PSObjectJsonConverter : JsonConverter + public override bool CanConvert(Type objectType) { - public override bool CanConvert(Type objectType) + return objectType == typeof(PSObject); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is not PSObject obj) + throw new ArgumentException(message: PSRuleResources.SerializeNullPSObject, paramName: nameof(value)); + + if (value is FileSystemInfo fileSystemInfo) { - return objectType == typeof(PSObject); + WriteFileSystemInfo(writer, fileSystemInfo, serializer); + return; } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + writer.WriteStartObject(); + foreach (var property in obj.Properties) { - if (!(value is PSObject obj)) - throw new ArgumentException(message: PSRuleResources.SerializeNullPSObject, paramName: nameof(value)); + // Ignore properties that are not readable or can cause race condition + if (!property.IsGettable || property.Value is PSDriveInfo || property.Value is ProviderInfo || property.Value is DirectoryInfo) + continue; - if (value is FileSystemInfo fileSystemInfo) - { - WriteFileSystemInfo(writer, fileSystemInfo, serializer); - return; - } - writer.WriteStartObject(); - foreach (var property in obj.Properties) - { - // Ignore properties that are not readable or can cause race condition - if (!property.IsGettable || property.Value is PSDriveInfo || property.Value is ProviderInfo || property.Value is DirectoryInfo) - continue; - - writer.WritePropertyName(property.Name); - serializer.Serialize(writer, property.Value); - } - writer.WriteEndObject(); + writer.WritePropertyName(property.Name); + serializer.Serialize(writer, property.Value); } + writer.WriteEndObject(); + } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // Create target object based on JObject + var result = existingValue as PSObject ?? new PSObject(); + + // Read tokens + ReadObject(value: result, reader: reader); + return result; + } + + private static void ReadObject(PSObject value, JsonReader reader) + { + if (reader.TokenType != JsonToken.StartObject) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); + + reader.Read(); + string name = null; + + // Read each token + while (reader.TokenType != JsonToken.EndObject) { - // Create target object based on JObject - var result = existingValue as PSObject ?? new PSObject(); - - // Read tokens - ReadObject(value: result, reader: reader); - return result; - } - - private static void ReadObject(PSObject value, JsonReader reader) - { - if (reader.TokenType != JsonToken.StartObject) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - - reader.Read(); - string name = null; - - // Read each token - while (reader.TokenType != JsonToken.EndObject) + switch (reader.TokenType) { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - name = reader.Value.ToString(); - break; + case JsonToken.PropertyName: + name = reader.Value.ToString(); + break; - case JsonToken.StartObject: - var child = new PSObject(); - ReadObject(value: child, reader: reader); - value.Properties.Add(new PSNoteProperty(name: name, value: child)); - break; + case JsonToken.StartObject: + var child = new PSObject(); + ReadObject(value: child, reader: reader); + value.Properties.Add(new PSNoteProperty(name: name, value: child)); + break; - case JsonToken.StartArray: - var items = new List(); + case JsonToken.StartArray: + var items = new List(); + reader.Read(); + + while (reader.TokenType != JsonToken.EndArray) + { + items.Add(ReadValue(reader)); reader.Read(); + } - while (reader.TokenType != JsonToken.EndArray) - { - items.Add(ReadValue(reader)); - reader.Read(); - } + value.Properties.Add(new PSNoteProperty(name: name, value: items.ToArray())); + break; - value.Properties.Add(new PSNoteProperty(name: name, value: items.ToArray())); - break; - - default: - value.Properties.Add(new PSNoteProperty(name: name, value: reader.Value)); - break; - } - reader.Read(); + default: + value.Properties.Add(new PSNoteProperty(name: name, value: reader.Value)); + break; } - } - - private static object ReadValue(JsonReader reader) - { - if (reader.TokenType != JsonToken.StartObject) - return reader.Value; - - var value = new PSObject(); - ReadObject(value, reader); - return value; - } - - private static void WriteFileSystemInfo(JsonWriter writer, FileSystemInfo value, JsonSerializer serializer) - { - serializer.Serialize(writer, value.FullName); + reader.Read(); } } - internal sealed class GitHubCommunityConverter : JsonConverter + private static object ReadValue(JsonReader reader) { - public override bool CanConvert(Type objectType) + if (reader.TokenType != JsonToken.StartObject) + return reader.Value; + + var value = new PSObject(); + ReadObject(value, reader); + return value; + } + + private static void WriteFileSystemInfo(JsonWriter writer, FileSystemInfo value, JsonSerializer serializer) + { + serializer.Serialize(writer, value.FullName); + } +} + +internal sealed class GitHubCommunityConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Data.CommunityProfile); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // Create target object based on JObject + var result = existingValue as Data.CommunityProfile ?? new Data.CommunityProfile(); + + // Read tokens + if (reader.TokenType != JsonToken.StartObject) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); + + reader.Read(); + string name = null; + + // Read each token + while (reader.TokenType != JsonToken.EndObject) { - return objectType == typeof(Data.CommunityProfile); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - // Create target object based on JObject - var result = existingValue as Data.CommunityProfile ?? new Data.CommunityProfile(); - - // Read tokens - if (reader.TokenType != JsonToken.StartObject) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - - reader.Read(); - string name = null; - - // Read each token - while (reader.TokenType != JsonToken.EndObject) + switch (reader.TokenType) { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - name = reader.Value.ToString(); - break; + case JsonToken.PropertyName: + name = reader.Value.ToString(); + break; - case JsonToken.StartObject: - if (name == "files") - { - ReadFiles(result, reader); - } - break; - } - reader.Read(); + case JsonToken.StartObject: + if (name == "files") + { + ReadFiles(result, reader); + } + break; } - return result; - } - - private static void ReadFiles(Data.CommunityProfile value, JsonReader reader) - { - if (reader.TokenType != JsonToken.StartObject) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - reader.Read(); - string name = null; + } + return result; + } - // Read each token - while (reader.TokenType != JsonToken.EndObject) + private static void ReadFiles(Data.CommunityProfile value, JsonReader reader) + { + if (reader.TokenType != JsonToken.StartObject) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); + + reader.Read(); + string name = null; + + // Read each token + while (reader.TokenType != JsonToken.EndObject) + { + switch (reader.TokenType) { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - name = reader.Value.ToString(); - break; + case JsonToken.PropertyName: + name = reader.Value.ToString(); + break; - case JsonToken.Null: - if (name == "issue_template") + case JsonToken.Null: + if (name == "issue_template") + value.IssueTemplate = true; + + break; + + case JsonToken.StartObject: + switch (name) + { + case "code_of_conduct": + value.CodeOfConduct = true; + break; + + case "contributing": + value.Contributing = true; + break; + + case "issue_template": value.IssueTemplate = true; + break; - break; + case "pull_request_template": + value.PullRequestTemplate = true; + break; - case JsonToken.StartObject: - switch (name) - { - case "code_of_conduct": - value.CodeOfConduct = true; - break; + case "license": + value.License = true; + break; - case "contributing": - value.Contributing = true; - break; + case "readme": + value.ReadMe = true; + break; - case "issue_template": - value.IssueTemplate = true; - break; - - case "pull_request_template": - value.PullRequestTemplate = true; - break; - - case "license": - value.License = true; - break; - - case "readme": - value.ReadMe = true; - break; - - default: - break; - } - reader.Skip(); - break; - } - reader.Read(); + default: + break; + } + reader.Skip(); + break; } + reader.Read(); } } } diff --git a/src/PSRule.Rules.GitHub/Common/PSObjectExtensions.cs b/src/PSRule.Rules.GitHub/Common/PSObjectExtensions.cs index aa518e0..8d90244 100644 --- a/src/PSRule.Rules.GitHub/Common/PSObjectExtensions.cs +++ b/src/PSRule.Rules.GitHub/Common/PSObjectExtensions.cs @@ -3,13 +3,12 @@ using System.Management.Automation; -namespace PSRule.Rules.GitHub +namespace PSRule.Rules.GitHub; + +internal static class PSObjectExtensions { - internal static class PSObjectExtensions + internal static T GetPropertyValue(this PSObject obj, string propertyName) { - internal static T GetPropertyValue(this PSObject obj, string propertyName) - { - return (T)obj.Properties[propertyName].Value; - } + return (T)obj.Properties[propertyName].Value; } } diff --git a/src/PSRule.Rules.GitHub/Configuration/ConfigurationOption.cs b/src/PSRule.Rules.GitHub/Configuration/ConfigurationOption.cs index 5cec70e..dc1080c 100644 --- a/src/PSRule.Rules.GitHub/Configuration/ConfigurationOption.cs +++ b/src/PSRule.Rules.GitHub/Configuration/ConfigurationOption.cs @@ -5,49 +5,48 @@ using System; using System.ComponentModel; using YamlDotNet.Serialization; -namespace PSRule.Rules.GitHub.Configuration +namespace PSRule.Rules.GitHub.Configuration; + +/// +/// A set of configuration values that can be used within rule definitions. +/// +public sealed class ConfigurationOption { - /// - /// A set of configuration values that can be used within rule definitions. - /// - public sealed class ConfigurationOption + public ConfigurationOption() { - public ConfigurationOption() - { - DefaultOrg = null; - } - - public ConfigurationOption(ConfigurationOption option) - { - if (option == null) - throw new ArgumentNullException(nameof(option)); - - DefaultOrg = option.DefaultOrg; - } - - public override bool Equals(object obj) - { - return obj is ConfigurationOption option && Equals(option); - } - - public bool Equals(ConfigurationOption other) - { - return other != null && - DefaultOrg == other.DefaultOrg; - } - - public override int GetHashCode() - { - unchecked // Overflow is fine - { - int hash = 17; - hash = hash * 23 + (DefaultOrg != null ? DefaultOrg.GetHashCode() : 0); - return hash; - } - } - - [DefaultValue(null)] - [YamlMember(Alias = "GitHub_DefaultOrg")] - public string DefaultOrg { get; set; } + DefaultOrg = null; } + + public ConfigurationOption(ConfigurationOption option) + { + if (option == null) + throw new ArgumentNullException(nameof(option)); + + DefaultOrg = option.DefaultOrg; + } + + public override bool Equals(object obj) + { + return obj is ConfigurationOption option && Equals(option); + } + + public bool Equals(ConfigurationOption other) + { + return other != null && + DefaultOrg == other.DefaultOrg; + } + + public override int GetHashCode() + { + unchecked // Overflow is fine + { + var hash = 17; + hash = hash * 23 + (DefaultOrg != null ? DefaultOrg.GetHashCode() : 0); + return hash; + } + } + + [DefaultValue(null)] + [YamlMember(Alias = "GitHub_DefaultOrg")] + public string DefaultOrg { get; set; } } diff --git a/src/PSRule.Rules.GitHub/Configuration/OutputEncoding.cs b/src/PSRule.Rules.GitHub/Configuration/OutputEncoding.cs index f407b9b..db8fc9c 100644 --- a/src/PSRule.Rules.GitHub/Configuration/OutputEncoding.cs +++ b/src/PSRule.Rules.GitHub/Configuration/OutputEncoding.cs @@ -4,21 +4,20 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace PSRule.Rules.GitHub.Configuration +namespace PSRule.Rules.GitHub.Configuration; + +[JsonConverter(typeof(StringEnumConverter))] +public enum OutputEncoding { - [JsonConverter(typeof(StringEnumConverter))] - public enum OutputEncoding - { - Default = 0, + Default = 0, - UTF8 = 1, + UTF8 = 1, - UTF7 = 2, + UTF7 = 2, - Unicode = 3, + Unicode = 3, - UTF32 = 4, + UTF32 = 4, - ASCII = 5 - } + ASCII = 5 } diff --git a/src/PSRule.Rules.GitHub/Configuration/OutputOption.cs b/src/PSRule.Rules.GitHub/Configuration/OutputOption.cs index 6421a87..2af9d85 100644 --- a/src/PSRule.Rules.GitHub/Configuration/OutputOption.cs +++ b/src/PSRule.Rules.GitHub/Configuration/OutputOption.cs @@ -4,68 +4,67 @@ using System; using System.ComponentModel; -namespace PSRule.Rules.GitHub.Configuration +namespace PSRule.Rules.GitHub.Configuration; + +/// +/// Options for generating and formatting output. +/// +public sealed class OutputOption : IEquatable { - /// - /// Options for generating and formatting output. - /// - public sealed class OutputOption : IEquatable + private const OutputEncoding DEFAULT_ENCODING = OutputEncoding.Default; + + internal static readonly OutputOption Default = new() { - private const OutputEncoding DEFAULT_ENCODING = OutputEncoding.Default; + Encoding = DEFAULT_ENCODING + }; - internal static readonly OutputOption Default = new OutputOption - { - Encoding = DEFAULT_ENCODING - }; - - public OutputOption() - { - Encoding = null; - Path = null; - } - - public OutputOption(OutputOption option) - { - if (option == null) - throw new ArgumentNullException(nameof(option)); - - Encoding = option.Encoding; - Path = option.Path; - } - - public override bool Equals(object obj) - { - return obj is OutputOption option && Equals(option); - } - - public bool Equals(OutputOption other) - { - return other != null && - Encoding == other.Encoding && - Path == other.Path; - } - - public override int GetHashCode() - { - unchecked // Overflow is fine - { - int hash = 17; - hash = hash * 23 + (Encoding.HasValue ? Encoding.Value.GetHashCode() : 0); - hash = hash * 23 + (Path != null ? Path.GetHashCode() : 0); - return hash; - } - } - - /// - /// The encoding to use when writing results to file. - /// - [DefaultValue(null)] - public OutputEncoding? Encoding { get; set; } - - /// - /// The file path location to save results. - /// - [DefaultValue(null)] - public string Path { get; set; } + public OutputOption() + { + Encoding = null; + Path = null; } + + public OutputOption(OutputOption option) + { + if (option == null) + throw new ArgumentNullException(nameof(option)); + + Encoding = option.Encoding; + Path = option.Path; + } + + public override bool Equals(object obj) + { + return obj is OutputOption option && Equals(option); + } + + public bool Equals(OutputOption other) + { + return other != null && + Encoding == other.Encoding && + Path == other.Path; + } + + public override int GetHashCode() + { + unchecked // Overflow is fine + { + var hash = 17; + hash = hash * 23 + (Encoding.HasValue ? Encoding.Value.GetHashCode() : 0); + hash = hash * 23 + (Path != null ? Path.GetHashCode() : 0); + return hash; + } + } + + /// + /// The encoding to use when writing results to file. + /// + [DefaultValue(null)] + public OutputEncoding? Encoding { get; set; } + + /// + /// The file path location to save results. + /// + [DefaultValue(null)] + public string Path { get; set; } } diff --git a/src/PSRule.Rules.GitHub/Configuration/PSRuleOption.cs b/src/PSRule.Rules.GitHub/Configuration/PSRuleOption.cs index 08b984b..daedf61 100644 --- a/src/PSRule.Rules.GitHub/Configuration/PSRuleOption.cs +++ b/src/PSRule.Rules.GitHub/Configuration/PSRuleOption.cs @@ -8,107 +8,106 @@ using System.Management.Automation; using System.Net; using System.Security; -namespace PSRule.Rules.GitHub.Configuration +namespace PSRule.Rules.GitHub.Configuration; + +/// +/// A delgate to allow callback to PowerShell to get current working path. +/// +internal delegate string PathDelegate(); + +public sealed class PSRuleOption { - /// - /// A delgate to allow callback to PowerShell to get current working path. - /// - internal delegate string PathDelegate(); - - public sealed class PSRuleOption + internal static readonly PSRuleOption Default = new() { - internal static readonly PSRuleOption Default = new PSRuleOption - { - Output = OutputOption.Default - }; + Output = OutputOption.Default + }; - /// - /// A callback that is overridden by PowerShell so that the current working path can be retrieved. - /// - private static PathDelegate _GetWorkingPath = () => Directory.GetCurrentDirectory(); + /// + /// A callback that is overridden by PowerShell so that the current working path can be retrieved. + /// + private static PathDelegate _GetWorkingPath = () => Directory.GetCurrentDirectory(); - public PSRuleOption() + public PSRuleOption() + { + // Set defaults + Configuration = new ConfigurationOption(); + Output = new OutputOption(); + } + + public ConfigurationOption Configuration { get; set; } + + /// + /// Options that affect how output is generated. + /// + public OutputOption Output { get; set; } + + /// + /// Set working path from PowerShell host environment. + /// + /// An $ExecutionContext object. + /// + /// Called from PowerShell. + /// + public static void UseExecutionContext(EngineIntrinsics executionContext) + { + if (executionContext == null) { - // Set defaults - Configuration = new ConfigurationOption(); - Output = new OutputOption(); + _GetWorkingPath = () => Directory.GetCurrentDirectory(); + return; } + _GetWorkingPath = () => executionContext.SessionState.Path.CurrentFileSystemLocation.Path; + } - public ConfigurationOption Configuration { get; set; } + public static string GetWorkingPath() + { + return _GetWorkingPath(); + } - /// - /// Options that affect how output is generated. - /// - public OutputOption Output { get; set; } + /// + /// Get a full path instead of a relative path that may be passed from PowerShell. + /// + internal static string GetRootedPath(string path) + { + return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(GetWorkingPath(), path)); + } - /// - /// Set working path from PowerShell host environment. - /// - /// An $ExecutionContext object. - /// - /// Called from PowerShell. - /// - public static void UseExecutionContext(EngineIntrinsics executionContext) - { - if (executionContext == null) - { - _GetWorkingPath = () => Directory.GetCurrentDirectory(); - return; - } - _GetWorkingPath = () => executionContext.SessionState.Path.CurrentFileSystemLocation.Path; - } + /// + /// Get a full path instead of a relative path that may be passed from PowerShell. + /// + internal static string GetRootedBasePath(string path) + { + var rootedPath = GetRootedPath(path); + if (rootedPath.Length > 0 && IsSeparator(rootedPath[rootedPath.Length - 1])) + return rootedPath; - public static string GetWorkingPath() - { - return _GetWorkingPath(); - } + return string.Concat(rootedPath, Path.DirectorySeparatorChar); + } - /// - /// Get a full path instead of a relative path that may be passed from PowerShell. - /// - internal static string GetRootedPath(string path) - { - return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(GetWorkingPath(), path)); - } + internal static bool TryGetEnvironmentVariableString(string variable, out string value) + { + value = null; + var v = Environment.GetEnvironmentVariable(variable); + if (string.IsNullOrEmpty(v)) + return false; - /// - /// Get a full path instead of a relative path that may be passed from PowerShell. - /// - internal static string GetRootedBasePath(string path) - { - var rootedPath = GetRootedPath(path); - if (rootedPath.Length > 0 && IsSeparator(rootedPath[rootedPath.Length - 1])) - return rootedPath; + value = v; + return true; + } - return string.Concat(rootedPath, Path.DirectorySeparatorChar); - } + internal static bool TryGetEnvironmentVariableSecureString(string variable, out SecureString value) + { + value = null; + var v = Environment.GetEnvironmentVariable(variable); + if (string.IsNullOrEmpty(v)) + return false; - internal static bool TryGetEnvironmentVariableString(string variable, out string value) - { - value = null; - var v = Environment.GetEnvironmentVariable(variable); - if (string.IsNullOrEmpty(v)) - return false; + value = new NetworkCredential("na", v).SecurePassword; + return true; + } - value = v; - return true; - } - - internal static bool TryGetEnvironmentVariableSecureString(string variable, out SecureString value) - { - value = null; - var v = Environment.GetEnvironmentVariable(variable); - if (string.IsNullOrEmpty(v)) - return false; - - value = new NetworkCredential("na", v).SecurePassword; - return true; - } - - [DebuggerStepThrough] - private static bool IsSeparator(char c) - { - return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; - } + [DebuggerStepThrough] + private static bool IsSeparator(char c) + { + return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; } } diff --git a/src/PSRule.Rules.GitHub/Data/Models.cs b/src/PSRule.Rules.GitHub/Data/Models.cs index e598404..aed5a9c 100644 --- a/src/PSRule.Rules.GitHub/Data/Models.cs +++ b/src/PSRule.Rules.GitHub/Data/Models.cs @@ -3,205 +3,204 @@ using System.Collections.Generic; -namespace PSRule.Rules.GitHub.Data +namespace PSRule.Rules.GitHub.Data; + +public sealed class Repository { - public sealed class Repository + private const string OBJECT_TYPE = "api.github.com/repos"; + + internal Repository(string owner, string name) { - private const string OBJECT_TYPE = "api.github.com/repos"; - - internal Repository(string owner, string name) - { - Owner = owner; - Name = name; - FullName = string.Concat(owner, '/', name); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Used for object serialization")] - public string Type => OBJECT_TYPE; - - public string Owner { get; } - - public string Name { get; } - - public string FullName { get; } - - public string RepositoryName => FullName; - - public string Description { get; internal set; } - - public bool Private { get; internal set; } - - public bool Fork { get; internal set; } - - public bool Archived { get; internal set; } - - public string DefaultBranch { get; internal set; } - - public string License { get; internal set; } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "Used for object serialization")] - public string HtmlUrl { get; internal set; } - - public string Homepage { get; internal set; } - - public string Language { get; internal set; } - - public bool? AllowRebaseMerge { get; internal set; } - - public bool? AllowSquashMerge { get; internal set; } - - public bool? AllowMergeCommit { get; internal set; } - - public bool HasIssues { get; internal set; } - - public bool HasWiki { get; internal set; } - - public bool HasDownloads { get; internal set; } - - public bool HasPages { get; internal set; } - - public CommunityProfile CommunityProfile { get; internal set; } - - public IEnumerable CommunityFiles { get; internal set; } - - public bool IsTemplate { get; internal set; } - - public bool? DeleteBranchOnMerge { get; internal set; } - - public string Visibility { get; internal set; } + Owner = owner; + Name = name; + FullName = string.Concat(owner, '/', name); } - public sealed class Branch - { - private const string OBJECT_TYPE = "api.github.com/repos/branches"; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Used for object serialization")] + public string Type => OBJECT_TYPE; - internal Branch(string repositoryName, string name) - { - Name = name; - RepositoryName = repositoryName; - } + public string Owner { get; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Used for object serialization")] - public string Type => OBJECT_TYPE; + public string Name { get; } - public string Name { get; } + public string FullName { get; } - public string BranchName => Name; + public string RepositoryName => FullName; - public string RepositoryName { get; } + public string Description { get; internal set; } - public BranchProtection Protection { get; internal set; } + public bool Private { get; internal set; } - public IEnumerable Status { get; internal set; } - } + public bool Fork { get; internal set; } - public sealed class BranchProtection - { + public bool Archived { get; internal set; } - internal BranchProtection(bool enabled) - { - Enabled = enabled; - } + public string DefaultBranch { get; internal set; } - public bool Enabled { get; } + public string License { get; internal set; } - public bool? EnforceAdmins { get; internal set; } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "Used for object serialization")] + public string HtmlUrl { get; internal set; } - public bool? RequireUpToDate { get; internal set; } + public string Homepage { get; internal set; } - public IEnumerable RequireStatusChecks { get; internal set; } + public string Language { get; internal set; } - public bool RequirePullRequestReviews { get; internal set; } + public bool? AllowRebaseMerge { get; internal set; } - public bool? DismissStaleReviews { get; internal set; } + public bool? AllowSquashMerge { get; internal set; } - public bool? RequireCodeOwnerReviews { get; internal set; } + public bool? AllowMergeCommit { get; internal set; } - public int? RequiredApprovingReviewCount { get; internal set; } - } + public bool HasIssues { get; internal set; } - public sealed class BranchStatus - { - internal BranchStatus(string name) - { - Name = name; - } + public bool HasWiki { get; internal set; } - public string Name { get; } + public bool HasDownloads { get; internal set; } - public string Status { get; internal set; } + public bool HasPages { get; internal set; } - public string Conclusion { get; internal set; } - } + public CommunityProfile CommunityProfile { get; internal set; } - public sealed class Label - { - internal Label(string name) - { - Name = name; - } + public IEnumerable CommunityFiles { get; internal set; } - public string Name { get; } + public bool IsTemplate { get; internal set; } - public string Description { get; internal set; } + public bool? DeleteBranchOnMerge { get; internal set; } - public string Color { get; internal set; } - - public bool Default { get; internal set; } - } - - public sealed class Milestone - { - internal Milestone(int number) - { - Number = number; - } - - public int Number { get; } - - public string Title { get; internal set; } - - public string Description { get; internal set; } - - public string State { get; internal set; } - } - - public sealed class Release - { - internal Release(string name) - { - Name = name; - } - - public string Name { get; } - - public bool Prerelease { get; internal set; } - - public string TagName { get; internal set; } - } - - public sealed class RepositoryTag - { - internal RepositoryTag(string name) - { - Name = name; - } - - public string Name { get; } - } - - public sealed class CommunityProfile - { - public bool CodeOfConduct { get; set; } - - public bool Contributing { get; set; } - - public bool IssueTemplate { get; set; } - - public bool PullRequestTemplate { get; set; } - - public bool License { get; set; } - - public bool ReadMe { get; set; } - } + public string Visibility { get; internal set; } +} + +public sealed class Branch +{ + private const string OBJECT_TYPE = "api.github.com/repos/branches"; + + internal Branch(string repositoryName, string name) + { + Name = name; + RepositoryName = repositoryName; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Used for object serialization")] + public string Type => OBJECT_TYPE; + + public string Name { get; } + + public string BranchName => Name; + + public string RepositoryName { get; } + + public BranchProtection Protection { get; internal set; } + + public IEnumerable Status { get; internal set; } +} + +public sealed class BranchProtection +{ + + internal BranchProtection(bool enabled) + { + Enabled = enabled; + } + + public bool Enabled { get; } + + public bool? EnforceAdmins { get; internal set; } + + public bool? RequireUpToDate { get; internal set; } + + public IEnumerable RequireStatusChecks { get; internal set; } + + public bool RequirePullRequestReviews { get; internal set; } + + public bool? DismissStaleReviews { get; internal set; } + + public bool? RequireCodeOwnerReviews { get; internal set; } + + public int? RequiredApprovingReviewCount { get; internal set; } +} + +public sealed class BranchStatus +{ + internal BranchStatus(string name) + { + Name = name; + } + + public string Name { get; } + + public string Status { get; internal set; } + + public string Conclusion { get; internal set; } +} + +public sealed class Label +{ + internal Label(string name) + { + Name = name; + } + + public string Name { get; } + + public string Description { get; internal set; } + + public string Color { get; internal set; } + + public bool Default { get; internal set; } +} + +public sealed class Milestone +{ + internal Milestone(int number) + { + Number = number; + } + + public int Number { get; } + + public string Title { get; internal set; } + + public string Description { get; internal set; } + + public string State { get; internal set; } +} + +public sealed class Release +{ + internal Release(string name) + { + Name = name; + } + + public string Name { get; } + + public bool Prerelease { get; internal set; } + + public string TagName { get; internal set; } +} + +public sealed class RepositoryTag +{ + internal RepositoryTag(string name) + { + Name = name; + } + + public string Name { get; } +} + +public sealed class CommunityProfile +{ + public bool CodeOfConduct { get; set; } + + public bool Contributing { get; set; } + + public bool IssueTemplate { get; set; } + + public bool PullRequestTemplate { get; set; } + + public bool License { get; set; } + + public bool ReadMe { get; set; } } diff --git a/src/PSRule.Rules.GitHub/Pipeline/Exceptions.cs b/src/PSRule.Rules.GitHub/Pipeline/Exceptions.cs index 44073e5..e1277f0 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/Exceptions.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/Exceptions.cs @@ -5,118 +5,117 @@ using System; using System.Runtime.Serialization; using System.Security.Permissions; -namespace PSRule.Rules.GitHub.Pipeline +namespace PSRule.Rules.GitHub.Pipeline; + +/// +/// A base class for all pipeline exceptions. +/// +public abstract class PipelineException : Exception { /// - /// A base class for all pipeline exceptions. + /// Creates a pipeline exception. /// - public abstract class PipelineException : Exception + protected PipelineException() { - /// - /// Creates a pipeline exception. - /// - protected PipelineException() - { - } - - /// - /// Creates a pipeline exception. - /// - /// The detail of the exception. - protected PipelineException(string message) - : base(message) { } - - /// - /// Creates a pipeline exception. - /// - /// The detail of the exception. - /// A nested exception that caused the issue. - protected PipelineException(string message, Exception innerException) - : base(message, innerException) { } - - protected PipelineException(SerializationInfo info, StreamingContext context) - : base(info, context) { } } /// - /// A serialization exception. + /// Creates a pipeline exception. /// - [Serializable] - public sealed class PipelineSerializationException : PipelineException + /// The detail of the exception. + protected PipelineException(string message) + : base(message) { } + + /// + /// Creates a pipeline exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + protected PipelineException(string message, Exception innerException) + : base(message, innerException) { } + + protected PipelineException(SerializationInfo info, StreamingContext context) + : base(info, context) { } +} + +/// +/// A serialization exception. +/// +[Serializable] +public sealed class PipelineSerializationException : PipelineException +{ + /// + /// Creates a serialization exception. + /// + public PipelineSerializationException() { - /// - /// Creates a serialization exception. - /// - public PipelineSerializationException() - { - } - - /// - /// Creates a serialization exception. - /// - /// The detail of the exception. - public PipelineSerializationException(string message) - : base(message) { } - - /// - /// Creates a serialization exception. - /// - /// The detail of the exception. - /// A nested exception that caused the issue. - public PipelineSerializationException(string message, Exception innerException) - : base(message, innerException) { } - - private PipelineSerializationException(SerializationInfo info, StreamingContext context) - : base(info, context) { } - - [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - throw new ArgumentNullException(nameof(info)); - - base.GetObjectData(info, context); - } } /// - /// An exception related to template linking. + /// Creates a serialization exception. /// - [Serializable] - public sealed class InvalidTemplateLinkException : PipelineException + /// The detail of the exception. + public PipelineSerializationException(string message) + : base(message) { } + + /// + /// Creates a serialization exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + public PipelineSerializationException(string message, Exception innerException) + : base(message, innerException) { } + + private PipelineSerializationException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) { - /// - /// Creates a template linking exception. - /// - public InvalidTemplateLinkException() - { - } + if (info == null) + throw new ArgumentNullException(nameof(info)); - /// - /// Creates a template linking exception. - /// - /// The detail of the exception. - public InvalidTemplateLinkException(string message) - : base(message) { } - - /// - /// Creates a template linking exception. - /// - /// The detail of the exception. - /// A nested exception that caused the issue. - public InvalidTemplateLinkException(string message, Exception innerException) - : base(message, innerException) { } - - private InvalidTemplateLinkException(SerializationInfo info, StreamingContext context) - : base(info, context) { } - - [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - throw new ArgumentNullException(nameof(info)); - - base.GetObjectData(info, context); - } + base.GetObjectData(info, context); + } +} + +/// +/// An exception related to template linking. +/// +[Serializable] +public sealed class InvalidTemplateLinkException : PipelineException +{ + /// + /// Creates a template linking exception. + /// + public InvalidTemplateLinkException() + { + } + + /// + /// Creates a template linking exception. + /// + /// The detail of the exception. + public InvalidTemplateLinkException(string message) + : base(message) { } + + /// + /// Creates a template linking exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + public InvalidTemplateLinkException(string message, Exception innerException) + : base(message, innerException) { } + + private InvalidTemplateLinkException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + throw new ArgumentNullException(nameof(info)); + + base.GetObjectData(info, context); } } diff --git a/src/PSRule.Rules.GitHub/Pipeline/ExportPipeline.cs b/src/PSRule.Rules.GitHub/Pipeline/ExportPipeline.cs index e685274..0af4dd7 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/ExportPipeline.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/ExportPipeline.cs @@ -3,175 +3,173 @@ using System; using System.Management.Automation; -using System.Security; using System.Text; using PSRule.Rules.GitHub.Configuration; using PSRule.Rules.GitHub.Pipeline.Output; -namespace PSRule.Rules.GitHub.Pipeline +namespace PSRule.Rules.GitHub.Pipeline; + +public interface IExportPipelineBuilder : IPipelineBuilder { - public interface IExportPipelineBuilder : IPipelineBuilder - { - void Repository(string[] repo); + void Repository(string[] repo); - void Credential(PSCredential credential); + void Credential(PSCredential credential); +} + +internal sealed class ExportPipelineBuilder : PipelineBuilderBase, IExportPipelineBuilder +{ + private const string OUTPUTFILE_PREFIX = "github-"; + private const string OUTPUTFILE_EXTENSION = ".json"; + + private const string GITHUB_REPOSITORY = "GITHUB_REPOSITORY"; + private const string GITHUB_TOKEN = "GITHUB_TOKEN"; + + private string[] _Repository; + private bool _UseGitHubToken; + private PSCredential _Credential; + private bool _PassThru; + + public ExportPipelineBuilder(PSRuleOption option) + { + _PassThru = false; + Configure(option); } - internal sealed class ExportPipelineBuilder : PipelineBuilderBase, IExportPipelineBuilder + public void Repository(string[] repository) { - private const string OUTPUTFILE_PREFIX = "github-"; - private const string OUTPUTFILE_EXTENSION = ".json"; - - private const string GITHUB_REPOSITORY = "GITHUB_REPOSITORY"; - private const string GITHUB_TOKEN = "GITHUB_TOKEN"; - - private string[] _Repository; - private bool _UseGitHubToken; - private PSCredential _Credential; - private bool _PassThru; - - public ExportPipelineBuilder(PSRuleOption option) + if (repository == null) { - _PassThru = false; - Configure(option); - } - - public void Repository(string[] repository) - { - if (repository == null) - { - if (PSRuleOption.TryGetEnvironmentVariableString(GITHUB_REPOSITORY, out var repo)) - _Repository = new string[] { repo }; - - return; - } - _Repository = repository; - } - - public void UseGitHubToken(bool useGitHubToken) - { - _UseGitHubToken = useGitHubToken; - } - - public void Credential(PSCredential credential) - { - if (_UseGitHubToken && credential == null) - { - if (PSRuleOption.TryGetEnvironmentVariableSecureString(GITHUB_TOKEN, out var token)) - _Credential = new PSCredential("token", token); - - return; - } - _Credential = credential; - } - - public void PassThru(bool passThru) - { - _PassThru = passThru; - } - - public override IPipeline Build() - { - return new ExportPipeline(PrepareContext(), PrepareWriter(), GetGitHubContext()); - } - - protected override PipelineWriter PrepareWriter() - { - return _PassThru ? base.PrepareWriter() : new JsonOutputWriter(GetOutput(), Option); - } - - protected override PipelineWriter GetOutput() - { - // Redirect to file instead - if (!string.IsNullOrEmpty(Option.Output.Path)) - { - return new FileOutputWriter( - inner: base.GetOutput(), - option: Option, - encoding: GetEncoding(Option.Output.Encoding), - path: Option.Output.Path, - defaultFile: string.Concat(OUTPUTFILE_PREFIX, Guid.NewGuid().ToString().Substring(0, 8), OUTPUTFILE_EXTENSION), - shouldProcess: CmdletContext.ShouldProcess - ); - } - return base.GetOutput(); - } - - private GitHubContext GetGitHubContext() - { - return new GitHubContext(_Repository, _Credential); - } - - /// - /// Get the character encoding for the specified output encoding. - /// - /// - /// - private static Encoding GetEncoding(OutputEncoding? encoding) - { - switch (encoding) - { - case OutputEncoding.UTF8: - return Encoding.UTF8; - - case OutputEncoding.UTF7: - return Encoding.UTF7; - - case OutputEncoding.Unicode: - return Encoding.Unicode; - - case OutputEncoding.UTF32: - return Encoding.UTF32; - - case OutputEncoding.ASCII: - return Encoding.ASCII; - - default: - return new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - } + if (PSRuleOption.TryGetEnvironmentVariableString(GITHUB_REPOSITORY, out var repo)) + _Repository = new string[] { repo }; + + return; } + _Repository = repository; } - internal sealed class ExportPipeline : PipelineBase + public void UseGitHubToken(bool useGitHubToken) { - private readonly GitHubContext _ServiceContext; - private readonly RepositoryHelper _Helper; + _UseGitHubToken = useGitHubToken; + } - // Track whether Dispose has been called. - private bool _Disposed; - - internal ExportPipeline(PipelineContext context, PipelineWriter writer, GitHubContext serviceContext) - : base(context, writer) + public void Credential(PSCredential credential) + { + if (_UseGitHubToken && credential == null) { - _ServiceContext = serviceContext; - _Helper = new RepositoryHelper(serviceContext); + if (PSRuleOption.TryGetEnvironmentVariableSecureString(GITHUB_TOKEN, out var token)) + _Credential = new PSCredential("token", token); + + return; } + _Credential = credential; + } - public override void End() + public void PassThru(bool passThru) + { + _PassThru = passThru; + } + + public override IPipeline Build() + { + return new ExportPipeline(PrepareContext(), PrepareWriter(), GetGitHubContext()); + } + + protected override PipelineWriter PrepareWriter() + { + return _PassThru ? base.PrepareWriter() : new JsonOutputWriter(GetOutput(), Option); + } + + protected override PipelineWriter GetOutput() + { + // Redirect to file instead + if (!string.IsNullOrEmpty(Option.Output.Path)) { - for (var i = 0; _ServiceContext.Repository != null && i < _ServiceContext.Repository.Length; i++) - ProcessRepository(_ServiceContext.Repository[i]); - - base.End(); + return new FileOutputWriter( + inner: base.GetOutput(), + option: Option, + encoding: GetEncoding(Option.Output.Encoding), + path: Option.Output.Path, + defaultFile: string.Concat(OUTPUTFILE_PREFIX, Guid.NewGuid().ToString().Substring(0, 8), OUTPUTFILE_EXTENSION), + shouldProcess: CmdletContext.ShouldProcess + ); } + return base.GetOutput(); + } - internal void ProcessRepository(string repositorySlug) + private GitHubContext GetGitHubContext() + { + return new GitHubContext(_Repository, _Credential); + } + + /// + /// Get the character encoding for the specified output encoding. + /// + /// + /// + private static Encoding GetEncoding(OutputEncoding? encoding) + { + switch (encoding) { - var o = _Helper.Get(repositorySlug); - if (o.Length > 0) - Writer.WriteObject(o, true); - } + case OutputEncoding.UTF8: + return Encoding.UTF8; - protected override void Dispose(bool disposing) - { - if (!_Disposed) - { - if (disposing) - _ServiceContext.Dispose(); + case OutputEncoding.UTF7: + return Encoding.UTF7; - _Disposed = true; - } - base.Dispose(disposing); + case OutputEncoding.Unicode: + return Encoding.Unicode; + + case OutputEncoding.UTF32: + return Encoding.UTF32; + + case OutputEncoding.ASCII: + return Encoding.ASCII; + + default: + return new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); } } } + +internal sealed class ExportPipeline : PipelineBase +{ + private readonly GitHubContext _ServiceContext; + private readonly RepositoryHelper _Helper; + + // Track whether Dispose has been called. + private bool _Disposed; + + internal ExportPipeline(PipelineContext context, PipelineWriter writer, GitHubContext serviceContext) + : base(context, writer) + { + _ServiceContext = serviceContext; + _Helper = new RepositoryHelper(serviceContext); + } + + public override void End() + { + for (var i = 0; _ServiceContext.Repository != null && i < _ServiceContext.Repository.Length; i++) + ProcessRepository(_ServiceContext.Repository[i]); + + base.End(); + } + + internal void ProcessRepository(string repositorySlug) + { + var o = _Helper.Get(repositorySlug); + if (o.Length > 0) + Writer.WriteObject(o, true); + } + + protected override void Dispose(bool disposing) + { + if (!_Disposed) + { + if (disposing) + _ServiceContext.Dispose(); + + _Disposed = true; + } + base.Dispose(disposing); + } +} diff --git a/src/PSRule.Rules.GitHub/Pipeline/GitHubClient.cs b/src/PSRule.Rules.GitHub/Pipeline/GitHubClient.cs index e14d79e..1b56d3b 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/GitHubClient.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/GitHubClient.cs @@ -8,370 +8,369 @@ using System.Net.Http; using System.Threading.Tasks; using PSRule.Rules.GitHub.Data; -namespace PSRule.Rules.GitHub.Pipeline +namespace PSRule.Rules.GitHub.Pipeline; + +internal sealed class GitHubClient { - internal sealed class GitHubClient + private readonly string[] _GITHUB_HEADERS_COMMUNITY_PROFILE = new string[] { "application/vnd.github.v3+json", "application/vnd.github.black-panther+json" }; + + private readonly Octokit.GitHubClient _Client; + private readonly HttpClient _HttpClient; + + #region Constructor + + public GitHubClient(GitHubContext serviceContext) { - private readonly string[] GITHUB_HEADERS_COMMUNITY_PROFILE = new string[] { "application/vnd.github.v3+json", "application/vnd.github.black-panther+json" }; + _Client = serviceContext.GetClient(); + _HttpClient = serviceContext.GetHttpClient(); + } - private readonly Octokit.GitHubClient _Client; - private readonly HttpClient _HttpClient; + #endregion Constructor - #region Constructor + #region Public methods - public GitHubClient(GitHubContext serviceContext) + public Repository[] GetRepository(string repositorySlug) + { + var items = GetRepositoryInternal(repositorySlug); + var results = new Repository[items.Length]; + for (var i = 0; i < items.Length; i++) { - _Client = serviceContext.GetClient(); - _HttpClient = serviceContext.GetHttpClient(); - } - - #endregion Constructor - - #region Public methods - - public Repository[] GetRepository(string repositorySlug) - { - var items = GetRepositoryInternal(repositorySlug); - var results = new Repository[items.Length]; - for (var i = 0; i < items.Length; i++) + var communityFiles = GetCommunityFiles(items[i].Owner.Login, items[i].Name); + var communityProfile = GetCommunityProfileInternal(items[i].Owner.Login, items[i].Name); + results[i] = new Repository(items[i].Owner.Login, items[i].Name) { - var communityFiles = GetCommunityFiles(items[i].Owner.Login, items[i].Name); - var communityProfile = GetCommunityProfileInternal(items[i].Owner.Login, items[i].Name); - results[i] = new Repository(items[i].Owner.Login, items[i].Name) + Description = items[i].Description, + Private = items[i].Private, + Fork = items[i].Fork, + Archived = items[i].Archived, + DefaultBranch = items[i].DefaultBranch, + License = items[i].License?.SpdxId, + HtmlUrl = items[i].HtmlUrl, + Homepage = items[i].Homepage, + Language = items[i].Language, + AllowMergeCommit = items[i].AllowMergeCommit, + AllowRebaseMerge = items[i].AllowRebaseMerge, + AllowSquashMerge = items[i].AllowSquashMerge, + HasIssues = items[i].HasIssues, + HasWiki = items[i].HasWiki, + HasDownloads = items[i].HasDownloads, + HasPages = items[i].HasPages, + CommunityProfile = communityProfile, + CommunityFiles = communityFiles, + IsTemplate = items[i].IsTemplate, + DeleteBranchOnMerge = items[i].DeleteBranchOnMerge, + Visibility = items[i].Visibility.HasValue ? Enum.GetName(typeof(Octokit.RepositoryVisibility), items[i].Visibility) : null, + }; + } + return results; + } + + public Branch[] GetBranches(string owner, string name) + { + var items = GetBranchesInternal(owner, name); + var results = new Data.Branch[items.Length]; + for (var i = 0; i < items.Length; i++) + { + var protection = items[i].Protected ? GetBranchProtectionInternal(owner, name, items[i].Name) : null; + var status = GetBranchStatus(owner, name, items[i].Name); + results[i] = new Branch(string.Concat(owner, '/', name), items[i].Name) + { + Protection = new BranchProtection(items[i].Protected) { - Description = items[i].Description, - Private = items[i].Private, - Fork = items[i].Fork, - Archived = items[i].Archived, - DefaultBranch = items[i].DefaultBranch, - License = items[i].License?.SpdxId, - HtmlUrl = items[i].HtmlUrl, - Homepage = items[i].Homepage, - Language = items[i].Language, - AllowMergeCommit = items[i].AllowMergeCommit, - AllowRebaseMerge = items[i].AllowRebaseMerge, - AllowSquashMerge = items[i].AllowSquashMerge, - HasIssues = items[i].HasIssues, - HasWiki = items[i].HasWiki, - HasDownloads = items[i].HasDownloads, - HasPages = items[i].HasPages, - CommunityProfile = communityProfile, - CommunityFiles = communityFiles, - IsTemplate = items[i].IsTemplate, - DeleteBranchOnMerge = items[i].DeleteBranchOnMerge, - Visibility = items[i].Visibility.HasValue ? Enum.GetName(typeof(Octokit.RepositoryVisibility), items[i].Visibility) : null, - }; - } - return results; + EnforceAdmins = protection?.EnforceAdmins?.Enabled, + RequireUpToDate = protection?.RequiredStatusChecks?.Strict, + RequireStatusChecks = protection?.RequiredStatusChecks?.Contexts?.ToArray(), + RequirePullRequestReviews = protection?.RequiredPullRequestReviews != null && protection?.RequiredPullRequestReviews?.RequiredApprovingReviewCount > 0, + DismissStaleReviews = protection?.RequiredPullRequestReviews?.DismissStaleReviews, + RequireCodeOwnerReviews = protection?.RequiredPullRequestReviews?.RequireCodeOwnerReviews, + RequiredApprovingReviewCount = protection?.RequiredPullRequestReviews?.RequiredApprovingReviewCount, + }, + Status = status, + }; } + return results; + } - public Branch[] GetBranches(string owner, string name) + public Label[] GetLabels(string owner, string name) + { + var items = GetLabelsInternal(owner, name); + var results = new Label[items.Length]; + for (var i = 0; i < items.Length; i++) { - var items = GetBranchesInternal(owner, name); - var results = new Data.Branch[items.Length]; - for (var i = 0; i < items.Length; i++) + results[i] = new Label(items[i].Name) { - var protection = items[i].Protected ? GetBranchProtectionInternal(owner, name, items[i].Name) : null; - var status = GetBranchStatus(owner, name, items[i].Name); - results[i] = new Branch(string.Concat(owner, '/', name), items[i].Name) - { - Protection = new BranchProtection(items[i].Protected) - { - EnforceAdmins = protection?.EnforceAdmins?.Enabled, - RequireUpToDate = protection?.RequiredStatusChecks?.Strict, - RequireStatusChecks = protection?.RequiredStatusChecks?.Contexts?.ToArray(), - RequirePullRequestReviews = protection?.RequiredPullRequestReviews != null && protection?.RequiredPullRequestReviews?.RequiredApprovingReviewCount > 0, - DismissStaleReviews = protection?.RequiredPullRequestReviews?.DismissStaleReviews, - RequireCodeOwnerReviews = protection?.RequiredPullRequestReviews?.RequireCodeOwnerReviews, - RequiredApprovingReviewCount = protection?.RequiredPullRequestReviews?.RequiredApprovingReviewCount, - }, - Status = status, - }; - } - return results; + Description = items[i].Description, + Color = items[i].Color, + Default = items[i].Default + }; } + return results; + } - public Label[] GetLabels(string owner, string name) + public Milestone[] GetMilestones(string owner, string name) + { + var items = GetMilestonesInternal(owner, name); + var results = new Milestone[items.Length]; + for (var i = 0; i < items.Length; i++) { - var items = GetLabelsInternal(owner, name); - var results = new Label[items.Length]; - for (var i = 0; i < items.Length; i++) + results[i] = new Milestone(items[i].Number) { - results[i] = new Label(items[i].Name) - { - Description = items[i].Description, - Color = items[i].Color, - Default = items[i].Default - }; - } - return results; + Title = items[i].Title, + Description = items[i].Description, + State = items[i].State.StringValue, + }; } + return results; + } - public Milestone[] GetMilestones(string owner, string name) + public Release[] GetReleases(string owner, string name) + { + var items = GetReleasesInternal(owner, name); + var results = new Release[items.Length]; + for (var i = 0; i < items.Length; i++) { - var items = GetMilestonesInternal(owner, name); - var results = new Milestone[items.Length]; - for (var i = 0; i < items.Length; i++) + results[i] = new Release(items[i].Name) { - results[i] = new Milestone(items[i].Number) - { - Title = items[i].Title, - Description = items[i].Description, - State = items[i].State.StringValue, - }; - } - return results; + Prerelease = items[i].Prerelease, + TagName = items[i].TagName, + }; } + return results; + } - public Release[] GetReleases(string owner, string name) + public RepositoryTag[] GetTags(string owner, string name) + { + var items = GetTagsInternal(owner, name); + var results = new RepositoryTag[items.Length]; + for (var i = 0; i < items.Length; i++) { - var items = GetReleasesInternal(owner, name); - var results = new Release[items.Length]; - for (var i = 0; i < items.Length; i++) + results[i] = new RepositoryTag(items[i].Name); + } + return results; + } + + #endregion Public methods + + #region Private methods + + private IEnumerable GetBranchStatus(string owner, string name, string branch) + { + var result = new List(); + var statuses = GetBranchStatusInternal(owner, name, branch); + for (var i = 0; i < statuses.Count; i++) + { + result.Add(new BranchStatus(statuses[i].Name) { - results[i] = new Release(items[i].Name) - { - Prerelease = items[i].Prerelease, - TagName = items[i].TagName, - }; - } - return results; + Status = statuses[i].Status.StringValue, + Conclusion = statuses[i].Conclusion?.StringValue, + }); } + return result; + } - public RepositoryTag[] GetTags(string owner, string name) + /// + /// Get matching repositories for the GitHub organization. + /// + private Octokit.Repository[] GetRepositoryInternal(string repositorySlug) + { + var slugParts = repositorySlug.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + var owner = slugParts[0]; + if (slugParts.Length == 2) + return GetSingleRepository(owner, slugParts[1]); + + var isOrg = IsOrg(owner); + return isOrg ? GetOrgRepository(owner) : GetUserRepository(owner); + } + + private Data.CommunityProfile GetCommunityProfileInternal(string owner, string name) + { + var profile = _HttpClient.Get($"https://api.github.com/repos/{owner}/{name}/community/profile", headers: _GITHUB_HEADERS_COMMUNITY_PROFILE); + return profile; + } + + /// + /// Get branches for the repository. + /// + private Octokit.Branch[] GetBranchesInternal(string owner, string name) + { + var task = _Client.Repository.Branch.GetAll(owner, name); + task.Wait(); + return task.Result.ToArray(); + } + + private Octokit.BranchProtectionSettings GetBranchProtectionInternal(string owner, string name, string branch) + { + try { - var items = GetTagsInternal(owner, name); - var results = new RepositoryTag[items.Length]; - for (var i = 0; i < items.Length; i++) - { - results[i] = new RepositoryTag(items[i].Name); - } - return results; - } - - #endregion Public methods - - #region Private methods - - private IEnumerable GetBranchStatus(string owner, string name, string branch) - { - var result = new List(); - var statuses = GetBranchStatusInternal(owner, name, branch); - for (var i = 0; i < statuses.Count; i++) - { - result.Add(new BranchStatus(statuses[i].Name) - { - Status = statuses[i].Status.StringValue, - Conclusion = statuses[i].Conclusion?.StringValue, - }); - } - return result; - } - - /// - /// Get matching repositories for the GitHub organization. - /// - private Octokit.Repository[] GetRepositoryInternal(string repositorySlug) - { - var slugParts = repositorySlug.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - var owner = slugParts[0]; - if (slugParts.Length == 2) - return GetSingleRepository(owner, slugParts[1]); - - var isOrg = IsOrg(owner); - return isOrg ? GetOrgRepository(owner) : GetUserRepository(owner); - } - - private Data.CommunityProfile GetCommunityProfileInternal(string owner, string name) - { - var profile = _HttpClient.Get($"https://api.github.com/repos/{owner}/{name}/community/profile", headers: GITHUB_HEADERS_COMMUNITY_PROFILE); - return profile; - } - - /// - /// Get branches for the repository. - /// - private Octokit.Branch[] GetBranchesInternal(string owner, string name) - { - var task = _Client.Repository.Branch.GetAll(owner, name); - task.Wait(); - return task.Result.ToArray(); - } - - private Octokit.BranchProtectionSettings GetBranchProtectionInternal(string owner, string name, string branch) - { - try - { - var task = _Client.Repository.Branch.GetBranchProtection(owner, name, branch); - task.Wait(); - return task.Result; - } - catch (AggregateException e) - { - var baseException = e.GetBaseException(); - if (baseException is Octokit.NotFoundException) - return null; - - // TODO: Should raise a warning - if (baseException is Octokit.ForbiddenException) - return null; - - throw; - } - } - - private IReadOnlyList GetBranchStatusInternal(string owner, string name, string branch) - { - try - { - var task = _Client.Check.Run.GetAllForReference(owner, name, branch); - task.Wait(); - return task.Result.CheckRuns; - } - catch (AggregateException e) - { - var baseException = e.GetBaseException(); - if (baseException is Octokit.NotFoundException) - return null; - - // TODO: Should raise a warning - if (baseException is Octokit.ForbiddenException) - return null; - - throw; - } - } - - /// - /// Get issue milestones for the repository. - /// - private Octokit.Milestone[] GetMilestonesInternal(string owner, string name) - { - var task = _Client.Issue.Milestone.GetAllForRepository(owner, name); - task.Wait(); - return task.Result.ToArray(); - } - - /// - /// Get releases for the repository. - /// - private Octokit.Release[] GetReleasesInternal(string owner, string name) - { - var task = _Client.Repository.Release.GetAll(owner, name); - task.Wait(); - return task.Result.ToArray(); - } - - /// - /// Get issue labels for the repository. - /// - private Octokit.Label[] GetLabelsInternal(string owner, string name) - { - var task = _Client.Issue.Labels.GetAllForRepository(owner, name); - task.Wait(); - return task.Result.ToArray(); - } - - private Octokit.RepositoryTag[] GetTagsInternal(string owner, string name) - { - var task = _Client.Repository.GetAllTags(owner, name); - task.Wait(); - return task.Result.ToArray(); - } - - private static readonly string[] GitHubPaths = new string[] - { - ".github", - ".github/ISSUE_TEMPLATE", - ".github/PULL_REQUEST_TEMPLATE", - "docs", - "docs/PULL_REQUEST_TEMPLATE", - "PULL_REQUEST_TEMPLATE" - }; - - private string[] GetCommunityFiles(string owner, string name) - { - var contentFiles = GetGitHubFiles(owner, name, GitHubPaths); - var files = new List(); - for (var i = 0; i < contentFiles.Length; i++) - IncludeCommunityFile(contentFiles[i], files); - - return files.ToArray(); - } - - private Octokit.RepositoryContent[] GetGitHubFiles(string owner, string name, string[] paths) - { - var tasks = new Task>[paths.Length + 1]; - tasks[0] = _Client.Repository.Content.GetAllContents(owner, name); - for (var i = 0; i < paths.Length; i++) - tasks[i + 1] = _Client.Repository.Content.GetAllContents(owner, name, paths[i]); - - try - { - Task.WaitAll(tasks); - } - catch (AggregateException) - { - // Discard AggregateExceptions for tasks - } - var result = new List(); - for (var i = 0; i < tasks.Length; i++) - { - if (!tasks[i].IsFaulted) - result.AddRange(tasks[i].Result); - } - return result.ToArray(); - } - - private static void IncludeCommunityFile(Octokit.RepositoryContent content, List files) - { - if (content.Type.Value == Octokit.ContentType.File && IsCommunityFile(content.Path)) - files.Add(content.Path); - } - - private static bool IsCommunityFile(string path) - { - return path.StartsWith(".github/", StringComparison.OrdinalIgnoreCase) || - path.StartsWith("README", StringComparison.OrdinalIgnoreCase) || path.StartsWith("LICENSE", StringComparison.OrdinalIgnoreCase) || - path.EndsWith("CODE_OF_CONDUCT.md", StringComparison.OrdinalIgnoreCase) || - path.EndsWith("CONTRIBUTING.md", StringComparison.OrdinalIgnoreCase) || - path.EndsWith("SECURITY.md", StringComparison.OrdinalIgnoreCase) || - path.EndsWith("SUPPORT.md", StringComparison.OrdinalIgnoreCase) || - path.EndsWith("pull_request_template.md", StringComparison.OrdinalIgnoreCase) || - path.EndsWith("pull_request_template.txt", StringComparison.OrdinalIgnoreCase); - } - - private Octokit.Repository[] GetUserRepository(string user) - { - var task = _Client.Repository.GetAllForUser(user); - task.Wait(); - return task.Result.ToArray(); - } - - private Octokit.Repository[] GetOrgRepository(string org) - { - var task = _Client.Repository.GetAllForOrg(org); - task.Wait(); - return task.Result.ToArray(); - } - - /// - /// Do a lookup against a login to determine if it is an organization. - /// - private bool IsOrg(string login) - { - var task = _Client.User.Get(login).ContinueWith(u => u.Result.Type == Octokit.AccountType.Organization); + var task = _Client.Repository.Branch.GetBranchProtection(owner, name, branch); task.Wait(); return task.Result; } - - private Octokit.Repository[] GetSingleRepository(string owner, string name) + catch (AggregateException e) { - var task = _Client.Repository.Get(owner, name).ContinueWith(r => r.Result); - task.Wait(); - return new Octokit.Repository[] { task.Result }; - } + var baseException = e.GetBaseException(); + if (baseException is Octokit.NotFoundException) + return null; - #endregion Private methods + // TODO: Should raise a warning + if (baseException is Octokit.ForbiddenException) + return null; + + throw; + } } + + private IReadOnlyList GetBranchStatusInternal(string owner, string name, string branch) + { + try + { + var task = _Client.Check.Run.GetAllForReference(owner, name, branch); + task.Wait(); + return task.Result.CheckRuns; + } + catch (AggregateException e) + { + var baseException = e.GetBaseException(); + if (baseException is Octokit.NotFoundException) + return null; + + // TODO: Should raise a warning + if (baseException is Octokit.ForbiddenException) + return null; + + throw; + } + } + + /// + /// Get issue milestones for the repository. + /// + private Octokit.Milestone[] GetMilestonesInternal(string owner, string name) + { + var task = _Client.Issue.Milestone.GetAllForRepository(owner, name); + task.Wait(); + return task.Result.ToArray(); + } + + /// + /// Get releases for the repository. + /// + private Octokit.Release[] GetReleasesInternal(string owner, string name) + { + var task = _Client.Repository.Release.GetAll(owner, name); + task.Wait(); + return task.Result.ToArray(); + } + + /// + /// Get issue labels for the repository. + /// + private Octokit.Label[] GetLabelsInternal(string owner, string name) + { + var task = _Client.Issue.Labels.GetAllForRepository(owner, name); + task.Wait(); + return task.Result.ToArray(); + } + + private Octokit.RepositoryTag[] GetTagsInternal(string owner, string name) + { + var task = _Client.Repository.GetAllTags(owner, name); + task.Wait(); + return task.Result.ToArray(); + } + + private static readonly string[] GitHubPaths = new string[] + { + ".github", + ".github/ISSUE_TEMPLATE", + ".github/PULL_REQUEST_TEMPLATE", + "docs", + "docs/PULL_REQUEST_TEMPLATE", + "PULL_REQUEST_TEMPLATE" + }; + + private string[] GetCommunityFiles(string owner, string name) + { + var contentFiles = GetGitHubFiles(owner, name, GitHubPaths); + var files = new List(); + for (var i = 0; i < contentFiles.Length; i++) + IncludeCommunityFile(contentFiles[i], files); + + return files.ToArray(); + } + + private Octokit.RepositoryContent[] GetGitHubFiles(string owner, string name, string[] paths) + { + var tasks = new Task>[paths.Length + 1]; + tasks[0] = _Client.Repository.Content.GetAllContents(owner, name); + for (var i = 0; i < paths.Length; i++) + tasks[i + 1] = _Client.Repository.Content.GetAllContents(owner, name, paths[i]); + + try + { + Task.WaitAll(tasks); + } + catch (AggregateException) + { + // Discard AggregateExceptions for tasks + } + var result = new List(); + for (var i = 0; i < tasks.Length; i++) + { + if (!tasks[i].IsFaulted) + result.AddRange(tasks[i].Result); + } + return result.ToArray(); + } + + private static void IncludeCommunityFile(Octokit.RepositoryContent content, List files) + { + if (content.Type.Value == Octokit.ContentType.File && IsCommunityFile(content.Path)) + files.Add(content.Path); + } + + private static bool IsCommunityFile(string path) + { + return path.StartsWith(".github/", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("README", StringComparison.OrdinalIgnoreCase) || path.StartsWith("LICENSE", StringComparison.OrdinalIgnoreCase) || + path.EndsWith("CODE_OF_CONDUCT.md", StringComparison.OrdinalIgnoreCase) || + path.EndsWith("CONTRIBUTING.md", StringComparison.OrdinalIgnoreCase) || + path.EndsWith("SECURITY.md", StringComparison.OrdinalIgnoreCase) || + path.EndsWith("SUPPORT.md", StringComparison.OrdinalIgnoreCase) || + path.EndsWith("pull_request_template.md", StringComparison.OrdinalIgnoreCase) || + path.EndsWith("pull_request_template.txt", StringComparison.OrdinalIgnoreCase); + } + + private Octokit.Repository[] GetUserRepository(string user) + { + var task = _Client.Repository.GetAllForUser(user); + task.Wait(); + return task.Result.ToArray(); + } + + private Octokit.Repository[] GetOrgRepository(string org) + { + var task = _Client.Repository.GetAllForOrg(org); + task.Wait(); + return task.Result.ToArray(); + } + + /// + /// Do a lookup against a login to determine if it is an organization. + /// + private bool IsOrg(string login) + { + var task = _Client.User.Get(login).ContinueWith(u => u.Result.Type == Octokit.AccountType.Organization); + task.Wait(); + return task.Result; + } + + private Octokit.Repository[] GetSingleRepository(string owner, string name) + { + var task = _Client.Repository.Get(owner, name).ContinueWith(r => r.Result); + task.Wait(); + return new Octokit.Repository[] { task.Result }; + } + + #endregion Private methods } diff --git a/src/PSRule.Rules.GitHub/Pipeline/GitHubContext.cs b/src/PSRule.Rules.GitHub/Pipeline/GitHubContext.cs index 365f46c..32e8966 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/GitHubContext.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/GitHubContext.cs @@ -9,61 +9,60 @@ using System.Net.Http.Headers; using System.Reflection; using Octokit; -namespace PSRule.Rules.GitHub.Pipeline +namespace PSRule.Rules.GitHub.Pipeline; + +internal sealed class GitHubContext : IDisposable { - internal sealed class GitHubContext : IDisposable + // Details for user-agent header + private const string GITHUB_PRODUCT_HEADER = "PSRule.Rules.GitHub"; + private static readonly string ProductVersion = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; + + private readonly PSCredential _Credential; + + private bool _Disposed; + + public GitHubContext(string[] repository, PSCredential credential) { - // Details for user-agent header - private const string GITHUB_PRODUCT_HEADER = "PSRule.Rules.GitHub"; - private static readonly string ProductVersion = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; + Repository = repository; + _Credential = credential; + } - private readonly PSCredential _Credential; + public string[] Repository { get; set; } - private bool _Disposed; + public Octokit.GitHubClient GetClient() + { + var client = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(GITHUB_PRODUCT_HEADER, ProductVersion)); + if (_Credential != null) + client.Credentials = new Credentials(_Credential.GetNetworkCredential().Password); - public GitHubContext(string[] repository, PSCredential credential) + return client; + } + + public HttpClient GetHttpClient() + { + var client = new HttpClient(); + if (_Credential != null) + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", _Credential.GetNetworkCredential().Password); + + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(GITHUB_PRODUCT_HEADER, ProductVersion)); + return client; + } + + private void Dispose(bool disposing) + { + if (!_Disposed) { - Repository = repository; - _Credential = credential; - } - - public string[] Repository { get; set; } - - public Octokit.GitHubClient GetClient() - { - var client = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(GITHUB_PRODUCT_HEADER, ProductVersion)); - if (_Credential != null) - client.Credentials = new Credentials(_Credential.GetNetworkCredential().Password); - - return client; - } - - public HttpClient GetHttpClient() - { - var client = new HttpClient(); - if (_Credential != null) - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", _Credential.GetNetworkCredential().Password); - - client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(GITHUB_PRODUCT_HEADER, ProductVersion)); - return client; - } - - private void Dispose(bool disposing) - { - if (!_Disposed) + if (disposing) { - if (disposing) - { - } - _Disposed = true; } - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _Disposed = true; } } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } diff --git a/src/PSRule.Rules.GitHub/Pipeline/LoggingExtensions.cs b/src/PSRule.Rules.GitHub/Pipeline/LoggingExtensions.cs index bc0349d..c2a18cf 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/LoggingExtensions.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/LoggingExtensions.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace PSRule.Rules.GitHub.Pipeline -{ - /// - /// Extensions for logging to the pipeline. - /// - internal static class LoggingExtensions - { +namespace PSRule.Rules.GitHub.Pipeline; + +/// +/// Extensions for logging to the pipeline. +/// +internal static class LoggingExtensions +{ - } } diff --git a/src/PSRule.Rules.GitHub/Pipeline/Output/FileOutputWriter.cs b/src/PSRule.Rules.GitHub/Pipeline/Output/FileOutputWriter.cs index fc7878f..e776ca7 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/Output/FileOutputWriter.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/Output/FileOutputWriter.cs @@ -6,48 +6,47 @@ using System.Text; using PSRule.Rules.GitHub.Configuration; using PSRule.Rules.GitHub.Resources; -namespace PSRule.Rules.GitHub.Pipeline.Output +namespace PSRule.Rules.GitHub.Pipeline.Output; + +/// +/// An output writer that writes output to disk. +/// +internal sealed class FileOutputWriter : PipelineWriter { - /// - /// An output writer that writes output to disk. - /// - internal sealed class FileOutputWriter : PipelineWriter + private readonly Encoding _Encoding; + private readonly string _Path; + private readonly string _DefaultFile; + private readonly ShouldProcess _ShouldProcess; + + internal FileOutputWriter(PipelineWriter inner, PSRuleOption option, Encoding encoding, string path, string defaultFile, ShouldProcess shouldProcess) + : base(inner, option) { - private readonly Encoding _Encoding; - private readonly string _Path; - private readonly string _DefaultFile; - private readonly ShouldProcess _ShouldProcess; + _Encoding = encoding; + _Path = path; + _DefaultFile = defaultFile; + _ShouldProcess = shouldProcess; + } - internal FileOutputWriter(PipelineWriter inner, PSRuleOption option, Encoding encoding, string path, string defaultFile, ShouldProcess shouldProcess) - : base(inner, option) + public override void WriteObject(object sendToPipeline, bool enumerateCollection) + { + WriteToFile(sendToPipeline); + } + + private void WriteToFile(object o) + { + var rootedPath = PSRuleOption.GetRootedPath(_Path); + if (!Path.HasExtension(rootedPath) || Directory.Exists(rootedPath)) + rootedPath = Path.Combine(rootedPath, _DefaultFile); + + var parentPath = Directory.GetParent(rootedPath); + if (!parentPath.Exists && _ShouldProcess(target: parentPath.FullName, action: PSRuleResources.ShouldCreatePath)) + Directory.CreateDirectory(path: parentPath.FullName); + + if (_ShouldProcess(target: rootedPath, action: PSRuleResources.ShouldWriteFile)) { - _Encoding = encoding; - _Path = path; - _DefaultFile = defaultFile; - _ShouldProcess = shouldProcess; - } - - public override void WriteObject(object sendToPipeline, bool enumerateCollection) - { - WriteToFile(sendToPipeline); - } - - private void WriteToFile(object o) - { - var rootedPath = PSRuleOption.GetRootedPath(_Path); - if (!Path.HasExtension(rootedPath) || Directory.Exists(rootedPath)) - rootedPath = Path.Combine(rootedPath, _DefaultFile); - - var parentPath = Directory.GetParent(rootedPath); - if (!parentPath.Exists && _ShouldProcess(target: parentPath.FullName, action: PSRuleResources.ShouldCreatePath)) - Directory.CreateDirectory(path: parentPath.FullName); - - if (_ShouldProcess(target: rootedPath, action: PSRuleResources.ShouldWriteFile)) - { - File.WriteAllText(path: rootedPath, contents: o.ToString(), encoding: _Encoding); - var info = new FileInfo(rootedPath); - base.WriteObject(info, false); - } + File.WriteAllText(path: rootedPath, contents: o.ToString(), encoding: _Encoding); + var info = new FileInfo(rootedPath); + base.WriteObject(info, false); } } } diff --git a/src/PSRule.Rules.GitHub/Pipeline/Output/JsonOutputWriter.cs b/src/PSRule.Rules.GitHub/Pipeline/Output/JsonOutputWriter.cs index d6eab91..ec09c07 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/Output/JsonOutputWriter.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/Output/JsonOutputWriter.cs @@ -4,21 +4,20 @@ using Newtonsoft.Json; using PSRule.Rules.GitHub.Configuration; -namespace PSRule.Rules.GitHub.Pipeline.Output -{ - internal sealed class JsonOutputWriter : SerializationOutputWriter - { - internal JsonOutputWriter(PipelineWriter inner, PSRuleOption option) - : base(inner, option) { } +namespace PSRule.Rules.GitHub.Pipeline.Output; - protected override string Serialize(object[] o) +internal sealed class JsonOutputWriter : SerializationOutputWriter +{ + internal JsonOutputWriter(PipelineWriter inner, PSRuleOption option) + : base(inner, option) { } + + protected override string Serialize(object[] o) + { + var settings = new JsonSerializerSettings { - var settings = new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore - }; - settings.Converters.Add(new PSObjectJsonConverter()); - return JsonConvert.SerializeObject(o, settings: settings); - } + NullValueHandling = NullValueHandling.Ignore + }; + settings.Converters.Add(new PSObjectJsonConverter()); + return JsonConvert.SerializeObject(o, settings: settings); } } diff --git a/src/PSRule.Rules.GitHub/Pipeline/Output/PSPipelineWriter.cs b/src/PSRule.Rules.GitHub/Pipeline/Output/PSPipelineWriter.cs index 1d8d707..a5c6169 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/Output/PSPipelineWriter.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/Output/PSPipelineWriter.cs @@ -5,172 +5,171 @@ using System; using System.Management.Automation; using PSRule.Rules.GitHub.Configuration; -namespace PSRule.Rules.GitHub.Pipeline.Output +namespace PSRule.Rules.GitHub.Pipeline.Output; + +/// +/// An output writer that returns output to the host PowerShell runspace. +/// +internal sealed class PSPipelineWriter : PipelineWriter { - /// - /// An output writer that returns output to the host PowerShell runspace. - /// - internal sealed class PSPipelineWriter : PipelineWriter + private const string Source = "PSRule"; + private const string HostTag = "PSHOST"; + + private Action _OnWriteWarning; + private Action _OnWriteVerbose; + private Action _OnWriteError; + private Action _OnWriteInformation; + private Action _OnWriteDebug; + internal Action OnWriteObject; + + private bool _LogError; + private bool _LogWarning; + private bool _LogVerbose; + private bool _LogInformation; + private bool _LogDebug; + + internal PSPipelineWriter(PSRuleOption option) + : base(null, option) { } + + internal void UseCommandRuntime(PSCmdlet commandRuntime) { - private const string Source = "PSRule"; - private const string HostTag = "PSHOST"; + if (commandRuntime == null) + return; - private Action OnWriteWarning; - private Action OnWriteVerbose; - private Action OnWriteError; - private Action OnWriteInformation; - private Action OnWriteDebug; - internal Action OnWriteObject; - - private bool _LogError; - private bool _LogWarning; - private bool _LogVerbose; - private bool _LogInformation; - private bool _LogDebug; - - internal PSPipelineWriter(PSRuleOption option) - : base(null, option) { } - - internal void UseCommandRuntime(PSCmdlet commandRuntime) - { - if (commandRuntime == null) - return; - - OnWriteVerbose = commandRuntime.WriteVerbose; - OnWriteWarning = commandRuntime.WriteWarning; - OnWriteError = commandRuntime.WriteError; - OnWriteInformation = commandRuntime.WriteInformation; - OnWriteDebug = commandRuntime.WriteDebug; - OnWriteObject = commandRuntime.WriteObject; - } - - internal void UseExecutionContext(EngineIntrinsics executionContext) - { - if (executionContext == null) - return; - - _LogError = GetPreferenceVariable(executionContext, ErrorPreference); - _LogWarning = GetPreferenceVariable(executionContext, WarningPreference); - _LogVerbose = GetPreferenceVariable(executionContext, VerbosePreference); - _LogInformation = GetPreferenceVariable(executionContext, InformationPreference); - _LogDebug = GetPreferenceVariable(executionContext, DebugPreference); - } - - private static bool GetPreferenceVariable(EngineIntrinsics executionContext, string variableName) - { - var preference = GetPreferenceVariable(executionContext.SessionState, variableName); - if (preference == ActionPreference.Ignore) - return false; - - return !(preference == ActionPreference.SilentlyContinue && ( - variableName == VerbosePreference || - variableName == DebugPreference) - ); - } - - #region Internal logging methods - - /// - /// Core methods to hand off to logger. - /// - /// A valid PowerShell error record. - public override void WriteError(ErrorRecord errorRecord) - { - if (OnWriteError == null || !ShouldWriteError()) - return; - - OnWriteError(errorRecord); - } - - /// - /// Core method to hand off verbose messages to logger. - /// - /// A message to log. - public override void WriteVerbose(string message) - { - if (OnWriteVerbose == null || !ShouldWriteVerbose()) - return; - - OnWriteVerbose(message); - } - - /// - /// Core method to hand off warning messages to logger. - /// - /// A message to log - public override void WriteWarning(string message) - { - if (OnWriteWarning == null || !ShouldWriteWarning()) - return; - - OnWriteWarning(message); - } - - /// - /// Core method to hand off information messages to logger. - /// - public override void WriteInformation(InformationRecord informationRecord) - { - if (OnWriteInformation == null || !ShouldWriteInformation()) - return; - - OnWriteInformation(informationRecord); - } - - /// - /// Core method to hand off debug messages to logger. - /// - public override void WriteDebug(DebugRecord debugRecord) - { - if (OnWriteDebug == null || !ShouldWriteDebug()) - return; - - OnWriteDebug(debugRecord.Message); - } - - public override void WriteObject(object sendToPipeline, bool enumerateCollection) - { - if (OnWriteObject == null) - return; - - OnWriteObject(sendToPipeline, enumerateCollection); - } - - public override void WriteHost(HostInformationMessage info) - { - if (OnWriteInformation == null) - return; - - var record = new InformationRecord(info, Source); - record.Tags.Add(HostTag); - OnWriteInformation(record); - } - - public override bool ShouldWriteVerbose() - { - return _LogVerbose; - } - - public override bool ShouldWriteInformation() - { - return _LogInformation; - } - - public override bool ShouldWriteDebug() - { - return _LogDebug; - } - - public override bool ShouldWriteError() - { - return _LogError; - } - - public override bool ShouldWriteWarning() - { - return _LogWarning; - } - - #endregion Internal logging methods + _OnWriteVerbose = commandRuntime.WriteVerbose; + _OnWriteWarning = commandRuntime.WriteWarning; + _OnWriteError = commandRuntime.WriteError; + _OnWriteInformation = commandRuntime.WriteInformation; + _OnWriteDebug = commandRuntime.WriteDebug; + OnWriteObject = commandRuntime.WriteObject; } + + internal void UseExecutionContext(EngineIntrinsics executionContext) + { + if (executionContext == null) + return; + + _LogError = GetPreferenceVariable(executionContext, ErrorPreference); + _LogWarning = GetPreferenceVariable(executionContext, WarningPreference); + _LogVerbose = GetPreferenceVariable(executionContext, VerbosePreference); + _LogInformation = GetPreferenceVariable(executionContext, InformationPreference); + _LogDebug = GetPreferenceVariable(executionContext, DebugPreference); + } + + private static bool GetPreferenceVariable(EngineIntrinsics executionContext, string variableName) + { + var preference = GetPreferenceVariable(executionContext.SessionState, variableName); + if (preference == ActionPreference.Ignore) + return false; + + return !(preference == ActionPreference.SilentlyContinue && ( + variableName == VerbosePreference || + variableName == DebugPreference) + ); + } + + #region Internal logging methods + + /// + /// Core methods to hand off to logger. + /// + /// A valid PowerShell error record. + public override void WriteError(ErrorRecord errorRecord) + { + if (_OnWriteError == null || !ShouldWriteError()) + return; + + _OnWriteError(errorRecord); + } + + /// + /// Core method to hand off verbose messages to logger. + /// + /// A message to log. + public override void WriteVerbose(string message) + { + if (_OnWriteVerbose == null || !ShouldWriteVerbose()) + return; + + _OnWriteVerbose(message); + } + + /// + /// Core method to hand off warning messages to logger. + /// + /// A message to log + public override void WriteWarning(string message) + { + if (_OnWriteWarning == null || !ShouldWriteWarning()) + return; + + _OnWriteWarning(message); + } + + /// + /// Core method to hand off information messages to logger. + /// + public override void WriteInformation(InformationRecord informationRecord) + { + if (_OnWriteInformation == null || !ShouldWriteInformation()) + return; + + _OnWriteInformation(informationRecord); + } + + /// + /// Core method to hand off debug messages to logger. + /// + public override void WriteDebug(DebugRecord debugRecord) + { + if (_OnWriteDebug == null || !ShouldWriteDebug()) + return; + + _OnWriteDebug(debugRecord.Message); + } + + public override void WriteObject(object sendToPipeline, bool enumerateCollection) + { + if (OnWriteObject == null) + return; + + OnWriteObject(sendToPipeline, enumerateCollection); + } + + public override void WriteHost(HostInformationMessage info) + { + if (_OnWriteInformation == null) + return; + + var record = new InformationRecord(info, Source); + record.Tags.Add(HostTag); + _OnWriteInformation(record); + } + + public override bool ShouldWriteVerbose() + { + return _LogVerbose; + } + + public override bool ShouldWriteInformation() + { + return _LogInformation; + } + + public override bool ShouldWriteDebug() + { + return _LogDebug; + } + + public override bool ShouldWriteError() + { + return _LogError; + } + + public override bool ShouldWriteWarning() + { + return _LogWarning; + } + + #endregion Internal logging methods } diff --git a/src/PSRule.Rules.GitHub/Pipeline/PipelineBuilder.cs b/src/PSRule.Rules.GitHub/Pipeline/PipelineBuilder.cs index bfda023..8991c66 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/PipelineBuilder.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/PipelineBuilder.cs @@ -6,152 +6,151 @@ using System.Management.Automation; using PSRule.Rules.GitHub.Configuration; using PSRule.Rules.GitHub.Pipeline.Output; -namespace PSRule.Rules.GitHub.Pipeline +namespace PSRule.Rules.GitHub.Pipeline; + +internal delegate bool ShouldProcess(string target, string action); + +/// +/// A helper class for building a pipeline from PowerShell. +/// +public static class PipelineBuilder { - internal delegate bool ShouldProcess(string target, string action); - - /// - /// A helper class for building a pipeline from PowerShell. - /// - public static class PipelineBuilder + public static IExportPipelineBuilder Export(PSRuleOption option) { - public static IExportPipelineBuilder Export(PSRuleOption option) - { - return new ExportPipelineBuilder(option); - } - } - - public interface IPipelineBuilder - { - void UseCommandRuntime(PSCmdlet commandRuntime); - - void UseExecutionContext(EngineIntrinsics executionContext); - - IPipelineBuilder Configure(PSRuleOption option); - - IPipeline Build(); - } - - public interface IPipeline - { - void Begin(); - - void Process(PSObject sourceObject); - - void End(); - } - - internal abstract class PipelineBuilderBase : IPipelineBuilder - { - private readonly PSPipelineWriter _Output; - - protected readonly PSRuleOption Option; - - protected PSCmdlet CmdletContext; - protected EngineIntrinsics ExecutionContext; - - protected PipelineBuilderBase() - { - Option = new PSRuleOption(); - _Output = new PSPipelineWriter(Option); - } - - public virtual void UseCommandRuntime(PSCmdlet commandRuntime) - { - CmdletContext = commandRuntime; - _Output.UseCommandRuntime(commandRuntime); - } - - public void UseExecutionContext(EngineIntrinsics executionContext) - { - ExecutionContext = executionContext; - _Output.UseExecutionContext(executionContext); - } - - public virtual IPipelineBuilder Configure(PSRuleOption option) - { - if (option == null) - return this; - - Option.Output = new OutputOption(option.Output); - return this; - } - - public abstract IPipeline Build(); - - protected PipelineContext PrepareContext() - { - return new PipelineContext(Option); - } - - protected virtual PipelineWriter PrepareWriter() - { - var writer = new PSPipelineWriter(Option); - writer.UseCommandRuntime(CmdletContext); - writer.UseExecutionContext(ExecutionContext); - return writer; - } - - protected virtual PipelineWriter GetOutput() - { - return _Output; - } - } - - internal abstract class PipelineBase : IDisposable, IPipeline - { - protected readonly PipelineContext Context; - protected readonly PipelineWriter Writer; - - // Track whether Dispose has been called. - private bool _Disposed = false; - - - protected PipelineBase(PipelineContext context, PipelineWriter writer) - { - Context = context; - Writer = writer; - } - - #region IPipeline - - public virtual void Begin() - { - // Do nothing - } - - public virtual void Process(PSObject sourceObject) - { - // Do nothing - } - - public virtual void End() - { - Writer.End(); - } - - #endregion IPipeline - - #region IDisposable - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_Disposed) - { - if (disposing) - { - // Do nothing - } - _Disposed = true; - } - } - - #endregion IDisposable + return new ExportPipelineBuilder(option); } } + +public interface IPipelineBuilder +{ + void UseCommandRuntime(PSCmdlet commandRuntime); + + void UseExecutionContext(EngineIntrinsics executionContext); + + IPipelineBuilder Configure(PSRuleOption option); + + IPipeline Build(); +} + +public interface IPipeline +{ + void Begin(); + + void Process(PSObject sourceObject); + + void End(); +} + +internal abstract class PipelineBuilderBase : IPipelineBuilder +{ + private readonly PSPipelineWriter _Output; + + protected readonly PSRuleOption Option; + + protected PSCmdlet CmdletContext; + protected EngineIntrinsics ExecutionContext; + + protected PipelineBuilderBase() + { + Option = new PSRuleOption(); + _Output = new PSPipelineWriter(Option); + } + + public virtual void UseCommandRuntime(PSCmdlet commandRuntime) + { + CmdletContext = commandRuntime; + _Output.UseCommandRuntime(commandRuntime); + } + + public void UseExecutionContext(EngineIntrinsics executionContext) + { + ExecutionContext = executionContext; + _Output.UseExecutionContext(executionContext); + } + + public virtual IPipelineBuilder Configure(PSRuleOption option) + { + if (option == null) + return this; + + Option.Output = new OutputOption(option.Output); + return this; + } + + public abstract IPipeline Build(); + + protected PipelineContext PrepareContext() + { + return new PipelineContext(Option); + } + + protected virtual PipelineWriter PrepareWriter() + { + var writer = new PSPipelineWriter(Option); + writer.UseCommandRuntime(CmdletContext); + writer.UseExecutionContext(ExecutionContext); + return writer; + } + + protected virtual PipelineWriter GetOutput() + { + return _Output; + } +} + +internal abstract class PipelineBase : IDisposable, IPipeline +{ + protected readonly PipelineContext Context; + protected readonly PipelineWriter Writer; + + // Track whether Dispose has been called. + private bool _Disposed = false; + + + protected PipelineBase(PipelineContext context, PipelineWriter writer) + { + Context = context; + Writer = writer; + } + + #region IPipeline + + public virtual void Begin() + { + // Do nothing + } + + public virtual void Process(PSObject sourceObject) + { + // Do nothing + } + + public virtual void End() + { + Writer.End(); + } + + #endregion IPipeline + + #region IDisposable + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_Disposed) + { + if (disposing) + { + // Do nothing + } + _Disposed = true; + } + } + + #endregion IDisposable +} diff --git a/src/PSRule.Rules.GitHub/Pipeline/PipelineContext.cs b/src/PSRule.Rules.GitHub/Pipeline/PipelineContext.cs index 2533b69..1020e99 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/PipelineContext.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/PipelineContext.cs @@ -3,15 +3,14 @@ using PSRule.Rules.GitHub.Configuration; -namespace PSRule.Rules.GitHub.Pipeline -{ - internal sealed class PipelineContext - { - internal readonly PSRuleOption Option; +namespace PSRule.Rules.GitHub.Pipeline; - public PipelineContext(PSRuleOption option) - { - Option = option; - } +internal sealed class PipelineContext +{ + internal readonly PSRuleOption Option; + + public PipelineContext(PSRuleOption option) + { + Option = option; } } diff --git a/src/PSRule.Rules.GitHub/Pipeline/PipelineWriter.cs b/src/PSRule.Rules.GitHub/Pipeline/PipelineWriter.cs index ebd5b08..01e2e18 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/PipelineWriter.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/PipelineWriter.cs @@ -7,181 +7,180 @@ using System.Management.Automation; using System.Threading; using PSRule.Rules.GitHub.Configuration; -namespace PSRule.Rules.GitHub.Pipeline +namespace PSRule.Rules.GitHub.Pipeline; + +internal interface ILogger { - internal interface ILogger - { - void WriteVerbose(string message); + void WriteVerbose(string message); - void WriteVerbose(string format, params object[] args); + void WriteVerbose(string format, params object[] args); +} + +internal abstract class PipelineWriter : ILogger +{ + protected const string ErrorPreference = "ErrorActionPreference"; + protected const string WarningPreference = "WarningPreference"; + protected const string VerbosePreference = "VerbosePreference"; + protected const string InformationPreference = "InformationPreference"; + protected const string DebugPreference = "DebugPreference"; + + private readonly PipelineWriter _Writer; + + protected readonly PSRuleOption Option; + + protected PipelineWriter(PipelineWriter inner, PSRuleOption option) + { + _Writer = inner; + Option = option; } - internal abstract class PipelineWriter : ILogger + public virtual void Begin() { - protected const string ErrorPreference = "ErrorActionPreference"; - protected const string WarningPreference = "WarningPreference"; - protected const string VerbosePreference = "VerbosePreference"; - protected const string InformationPreference = "InformationPreference"; - protected const string DebugPreference = "DebugPreference"; + if (_Writer == null) + return; - private readonly PipelineWriter _Writer; - - protected readonly PSRuleOption Option; - - protected PipelineWriter(PipelineWriter inner, PSRuleOption option) - { - _Writer = inner; - Option = option; - } - - public virtual void Begin() - { - if (_Writer == null) - return; - - _Writer.Begin(); - } - - public virtual void WriteObject(object sendToPipeline, bool enumerateCollection) - { - if (_Writer == null || sendToPipeline == null) - return; - - _Writer.WriteObject(sendToPipeline, enumerateCollection); - } - - public virtual void End() - { - if (_Writer == null) - return; - - _Writer.End(); - } - - public void WriteVerbose(string format, params object[] args) - { - if (!ShouldWriteVerbose()) - return; - - WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, format, args)); - } - - public virtual void WriteVerbose(string message) - { - if (_Writer == null || string.IsNullOrEmpty(message)) - return; - - _Writer.WriteVerbose(message); - } - - public virtual bool ShouldWriteVerbose() - { - return _Writer != null && _Writer.ShouldWriteVerbose(); - } - - public virtual void WriteWarning(string message) - { - if (_Writer == null || string.IsNullOrEmpty(message)) - return; - - _Writer.WriteWarning(message); - } - - public virtual bool ShouldWriteWarning() - { - return _Writer != null && _Writer.ShouldWriteWarning(); - } - - public void WriteError(Exception exception, string errorId, ErrorCategory errorCategory, object targetObject) - { - if (!ShouldWriteError()) - return; - - WriteError(new ErrorRecord(exception, errorId, errorCategory, targetObject)); - } - - public virtual void WriteError(ErrorRecord errorRecord) - { - if (_Writer == null || errorRecord == null) - return; - - _Writer.WriteError(errorRecord); - } - - public virtual bool ShouldWriteError() - { - return _Writer != null && _Writer.ShouldWriteError(); - } - - public virtual void WriteInformation(InformationRecord informationRecord) - { - if (_Writer == null || informationRecord == null) - return; - - _Writer.WriteInformation(informationRecord); - } - - public virtual void WriteHost(HostInformationMessage info) - { - if (_Writer == null) - return; - - _Writer.WriteHost(info); - } - - public virtual bool ShouldWriteInformation() - { - return _Writer != null && _Writer.ShouldWriteInformation(); - } - - public virtual void WriteDebug(DebugRecord debugRecord) - { - if (_Writer == null || debugRecord == null) - return; - - _Writer.WriteDebug(debugRecord); - } - - public virtual bool ShouldWriteDebug() - { - return _Writer != null && _Writer.ShouldWriteDebug(); - } - - protected static ActionPreference GetPreferenceVariable(SessionState sessionState, string variableName) - { - return (ActionPreference)sessionState.PSVariable.GetValue(variableName); - } + _Writer.Begin(); } - internal abstract class SerializationOutputWriter : PipelineWriter + public virtual void WriteObject(object sendToPipeline, bool enumerateCollection) { - private readonly List _Result; + if (_Writer == null || sendToPipeline == null) + return; - protected SerializationOutputWriter(PipelineWriter inner, PSRuleOption option) - : base(inner, option) - { - _Result = new List(); - } + _Writer.WriteObject(sendToPipeline, enumerateCollection); + } - public override void WriteObject(object sendToPipeline, bool enumerateCollection) - { - Add(sendToPipeline); - } + public virtual void End() + { + if (_Writer == null) + return; - protected void Add(object o) - { - if (o is T[] collection) - _Result.AddRange(collection); - else if (o is T item) - _Result.Add(item); - } + _Writer.End(); + } - public sealed override void End() - { - var results = _Result.ToArray(); - base.WriteObject(Serialize(results), false); - } + public void WriteVerbose(string format, params object[] args) + { + if (!ShouldWriteVerbose()) + return; - protected abstract string Serialize(T[] o); + WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, format, args)); + } + + public virtual void WriteVerbose(string message) + { + if (_Writer == null || string.IsNullOrEmpty(message)) + return; + + _Writer.WriteVerbose(message); + } + + public virtual bool ShouldWriteVerbose() + { + return _Writer != null && _Writer.ShouldWriteVerbose(); + } + + public virtual void WriteWarning(string message) + { + if (_Writer == null || string.IsNullOrEmpty(message)) + return; + + _Writer.WriteWarning(message); + } + + public virtual bool ShouldWriteWarning() + { + return _Writer != null && _Writer.ShouldWriteWarning(); + } + + public void WriteError(Exception exception, string errorId, ErrorCategory errorCategory, object targetObject) + { + if (!ShouldWriteError()) + return; + + WriteError(new ErrorRecord(exception, errorId, errorCategory, targetObject)); + } + + public virtual void WriteError(ErrorRecord errorRecord) + { + if (_Writer == null || errorRecord == null) + return; + + _Writer.WriteError(errorRecord); + } + + public virtual bool ShouldWriteError() + { + return _Writer != null && _Writer.ShouldWriteError(); + } + + public virtual void WriteInformation(InformationRecord informationRecord) + { + if (_Writer == null || informationRecord == null) + return; + + _Writer.WriteInformation(informationRecord); + } + + public virtual void WriteHost(HostInformationMessage info) + { + if (_Writer == null) + return; + + _Writer.WriteHost(info); + } + + public virtual bool ShouldWriteInformation() + { + return _Writer != null && _Writer.ShouldWriteInformation(); + } + + public virtual void WriteDebug(DebugRecord debugRecord) + { + if (_Writer == null || debugRecord == null) + return; + + _Writer.WriteDebug(debugRecord); + } + + public virtual bool ShouldWriteDebug() + { + return _Writer != null && _Writer.ShouldWriteDebug(); + } + + protected static ActionPreference GetPreferenceVariable(SessionState sessionState, string variableName) + { + return (ActionPreference)sessionState.PSVariable.GetValue(variableName); } } + +internal abstract class SerializationOutputWriter : PipelineWriter +{ + private readonly List _Result; + + protected SerializationOutputWriter(PipelineWriter inner, PSRuleOption option) + : base(inner, option) + { + _Result = new List(); + } + + public override void WriteObject(object sendToPipeline, bool enumerateCollection) + { + Add(sendToPipeline); + } + + protected void Add(object o) + { + if (o is T[] collection) + _Result.AddRange(collection); + else if (o is T item) + _Result.Add(item); + } + + public sealed override void End() + { + var results = _Result.ToArray(); + base.WriteObject(Serialize(results), false); + } + + protected abstract string Serialize(T[] o); +} diff --git a/src/PSRule.Rules.GitHub/Pipeline/RepositoryHelper.cs b/src/PSRule.Rules.GitHub/Pipeline/RepositoryHelper.cs index 5f12435..53050d2 100644 --- a/src/PSRule.Rules.GitHub/Pipeline/RepositoryHelper.cs +++ b/src/PSRule.Rules.GitHub/Pipeline/RepositoryHelper.cs @@ -4,51 +4,50 @@ using System.Collections.Generic; using System.Management.Automation; -namespace PSRule.Rules.GitHub.Pipeline +namespace PSRule.Rules.GitHub.Pipeline; + +internal sealed class RepositoryHelper { - internal sealed class RepositoryHelper + private const string PROPERTY_BRANCHES = "Branches"; + private const string PROPERTY_LABELS = "Labels"; + private const string PROPERTY_MILESTONES = "Milestones"; + private const string PROPERTY_RELEASES = "Releases"; + private const string PROPERTY_TAGS = "Tags"; + + private readonly GitHubClient _Client; + + public RepositoryHelper(GitHubContext serviceContext) { - private const string PROPERTY_BRANCHES = "Branches"; - private const string PROPERTY_LABELS = "Labels"; - private const string PROPERTY_MILESTONES = "Milestones"; - private const string PROPERTY_RELEASES = "Releases"; - private const string PROPERTY_TAGS = "Tags"; + _Client = new GitHubClient(serviceContext); + } - private readonly GitHubClient _Client; - - public RepositoryHelper(GitHubContext serviceContext) + public PSObject[] Get(string repositorySlug) + { + var result = new List(); + var repos = _Client.GetRepository(repositorySlug); + for (var r = 0; r < repos.Length; r++) { - _Client = new GitHubClient(serviceContext); - } + var repo = PSObject.AsPSObject(repos[r]); + var branches = _Client.GetBranches(repos[r].Owner, repos[r].Name); + var labels = _Client.GetLabels(repos[r].Owner, repos[r].Name); + var milestones = _Client.GetMilestones(repos[r].Owner, repos[r].Name); + var releases = _Client.GetReleases(repos[r].Owner, repos[r].Name); + var tags = _Client.GetTags(repos[r].Owner, repos[r].Name); - public PSObject[] Get(string repositorySlug) - { - var result = new List(); - var repos = _Client.GetRepository(repositorySlug); - for (var r = 0; r < repos.Length; r++) + repo.Properties.Add(new PSNoteProperty(PROPERTY_BRANCHES, branches)); + repo.Properties.Add(new PSNoteProperty(PROPERTY_LABELS, labels)); + repo.Properties.Add(new PSNoteProperty(PROPERTY_MILESTONES, milestones)); + repo.Properties.Add(new PSNoteProperty(PROPERTY_RELEASES, releases)); + repo.Properties.Add(new PSNoteProperty(PROPERTY_TAGS, tags)); + result.Add(repo); + + // Write branches as separate objects + for (var b = 0; b < branches.Length; b++) { - var repo = PSObject.AsPSObject(repos[r]); - var branches = _Client.GetBranches(repos[r].Owner, repos[r].Name); - var labels = _Client.GetLabels(repos[r].Owner, repos[r].Name); - var milestones = _Client.GetMilestones(repos[r].Owner, repos[r].Name); - var releases = _Client.GetReleases(repos[r].Owner, repos[r].Name); - var tags = _Client.GetTags(repos[r].Owner, repos[r].Name); - - repo.Properties.Add(new PSNoteProperty(PROPERTY_BRANCHES, branches)); - repo.Properties.Add(new PSNoteProperty(PROPERTY_LABELS, labels)); - repo.Properties.Add(new PSNoteProperty(PROPERTY_MILESTONES, milestones)); - repo.Properties.Add(new PSNoteProperty(PROPERTY_RELEASES, releases)); - repo.Properties.Add(new PSNoteProperty(PROPERTY_TAGS, tags)); - result.Add(repo); - - // Write branches as separate objects - for (var b = 0; b < branches.Length; b++) - { - var branch = PSObject.AsPSObject(branches[b]); - result.Add(branch); - } + var branch = PSObject.AsPSObject(branches[b]); + result.Add(branch); } - return result.ToArray(); } + return result.ToArray(); } } diff --git a/src/PSRule.Rules.GitHub/Runtime/Helper.cs b/src/PSRule.Rules.GitHub/Runtime/Helper.cs index 253e3c8..a2c6d4e 100644 --- a/src/PSRule.Rules.GitHub/Runtime/Helper.cs +++ b/src/PSRule.Rules.GitHub/Runtime/Helper.cs @@ -3,30 +3,28 @@ using System; using System.Management.Automation; -using System.Security; using PSRule.Rules.GitHub.Configuration; using PSRule.Rules.GitHub.Pipeline; -namespace PSRule.Rules.GitHub.Runtime +namespace PSRule.Rules.GitHub.Runtime; + +/// +/// External helper to be referenced within rules. +/// +public static class Helper { - /// - /// External helper to be referenced within rules. - /// - public static class Helper + private const string GITHUB_REPOSITORY = "GITHUB_REPOSITORY"; + private const string GITHUB_TOKEN = "GITHUB_TOKEN"; + + public static PSObject[] GetRepository() { - private const string GITHUB_REPOSITORY = "GITHUB_REPOSITORY"; - private const string GITHUB_TOKEN = "GITHUB_TOKEN"; + var repos = PSRuleOption.TryGetEnvironmentVariableString(GITHUB_REPOSITORY, out var repo) ? new string[] { repo } : null; + var credential = PSRuleOption.TryGetEnvironmentVariableSecureString(GITHUB_TOKEN, out var token) ? new PSCredential("token", token) : null; + if (repos == null || repos.Length == 0 || credential == null) + return Array.Empty(); - public static PSObject[] GetRepository() - { - var repos = PSRuleOption.TryGetEnvironmentVariableString(GITHUB_REPOSITORY, out var repo) ? new string[] { repo } : null; - var credential = PSRuleOption.TryGetEnvironmentVariableSecureString(GITHUB_TOKEN, out var token) ? new PSCredential("token", token) : null; - if (repos == null || repos.Length == 0 || credential == null) - return Array.Empty(); - - var context = new GitHubContext(repos, credential); - var helper = new RepositoryHelper(context); - return helper.Get(repo); - } + var context = new GitHubContext(repos, credential); + var helper = new RepositoryHelper(context); + return helper.Get(repo); } } diff --git a/tests/PSRule.Rules.GitHub.Tests/PSRule.Rules.GitHub.Tests.csproj b/tests/PSRule.Rules.GitHub.Tests/PSRule.Rules.GitHub.Tests.csproj index 1705fa4..6004096 100644 --- a/tests/PSRule.Rules.GitHub.Tests/PSRule.Rules.GitHub.Tests.csproj +++ b/tests/PSRule.Rules.GitHub.Tests/PSRule.Rules.GitHub.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net7.0 {9ff459f0-c7bc-4936-9fc3-4d92b30b02a1} true false @@ -11,6 +11,8 @@ + + all