Update to build process and tool chain (#231)

This commit is contained in:
Bernie White 2024-01-17 23:16:48 +10:00 коммит произвёл GitHub
Родитель 6a60136b96
Коммит 260cdb4de0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
46 изменённых файлов: 2288 добавлений и 2470 удалений

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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'))

Просмотреть файл

@ -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')

Просмотреть файл

@ -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')

Просмотреть файл

@ -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
]
}

Просмотреть файл

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

89
.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

145
.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

7
.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

27
.github/workflows/first-interaction.yaml поставляемый Normal file
Просмотреть файл

@ -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.'

45
.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

Просмотреть файл

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

13
.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"
]
}

101
.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
}

164
.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": []
}
]
}

Просмотреть файл

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

Просмотреть файл

@ -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'
```

Просмотреть файл

@ -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"
}
}
}

Просмотреть файл

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

Просмотреть файл

@ -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: []

Просмотреть файл

Просмотреть файл

@ -3,7 +3,8 @@
<!-- Project defaults -->
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<LangVersion>12.0</LangVersion>
<!-- <Nullable>enable</Nullable> -->
<NeutralLanguage>en-US</NeutralLanguage>
<DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType>

Просмотреть файл

@ -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<string, object> 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<string, object> dictionary, IEnumerable<KeyValuePair<string, object>> 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<string, object> dictionary, string key, out object value)
{
return dictionary.TryGetValue(key, out value) && dictionary.Remove(key);
}
[DebuggerStepThrough]
public static void AddUnique(this IDictionary<string, object> dictionary, IEnumerable<KeyValuePair<string, object>> values)
{
foreach (var kv in values)
if (!dictionary.ContainsKey(kv.Key))
dictionary.Add(kv.Key, kv.Value);
}
}

Просмотреть файл

@ -4,28 +4,27 @@
using System.Net.Http;
using Newtonsoft.Json;
namespace PSRule.Rules.GitHub
{
internal static class HttpClientExtensions
{
public static T Get<T>(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<T>(contentTask.Result, settings);
}
internal static class HttpClientExtensions
{
public static T Get<T>(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<T>(contentTask.Result, settings);
}
}

Просмотреть файл

@ -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;
/// <summary>
/// A custom serializer to correctly convert PSObject properties to JSON instead of CLIXML.
/// </summary>
internal sealed class PSObjectJsonConverter : JsonConverter
{
/// <summary>
/// A custom serializer to correctly convert PSObject properties to JSON instead of CLIXML.
/// </summary>
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<object>();
case JsonToken.StartArray:
var items = new List<object>();
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();
}
}
}

Просмотреть файл

@ -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<T>(this PSObject obj, string propertyName)
{
internal static T GetPropertyValue<T>(this PSObject obj, string propertyName)
{
return (T)obj.Properties[propertyName].Value;
}
return (T)obj.Properties[propertyName].Value;
}
}

Просмотреть файл

@ -5,49 +5,48 @@ using System;
using System.ComponentModel;
using YamlDotNet.Serialization;
namespace PSRule.Rules.GitHub.Configuration
namespace PSRule.Rules.GitHub.Configuration;
/// <summary>
/// A set of configuration values that can be used within rule definitions.
/// </summary>
public sealed class ConfigurationOption
{
/// <summary>
/// A set of configuration values that can be used within rule definitions.
/// </summary>
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; }
}

Просмотреть файл

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

Просмотреть файл

@ -4,68 +4,67 @@
using System;
using System.ComponentModel;
namespace PSRule.Rules.GitHub.Configuration
namespace PSRule.Rules.GitHub.Configuration;
/// <summary>
/// Options for generating and formatting output.
/// </summary>
public sealed class OutputOption : IEquatable<OutputOption>
{
/// <summary>
/// Options for generating and formatting output.
/// </summary>
public sealed class OutputOption : IEquatable<OutputOption>
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;
}
}
/// <summary>
/// The encoding to use when writing results to file.
/// </summary>
[DefaultValue(null)]
public OutputEncoding? Encoding { get; set; }
/// <summary>
/// The file path location to save results.
/// </summary>
[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;
}
}
/// <summary>
/// The encoding to use when writing results to file.
/// </summary>
[DefaultValue(null)]
public OutputEncoding? Encoding { get; set; }
/// <summary>
/// The file path location to save results.
/// </summary>
[DefaultValue(null)]
public string Path { get; set; }
}

Просмотреть файл

@ -8,107 +8,106 @@ using System.Management.Automation;
using System.Net;
using System.Security;
namespace PSRule.Rules.GitHub.Configuration
namespace PSRule.Rules.GitHub.Configuration;
/// <summary>
/// A delgate to allow callback to PowerShell to get current working path.
/// </summary>
internal delegate string PathDelegate();
public sealed class PSRuleOption
{
/// <summary>
/// A delgate to allow callback to PowerShell to get current working path.
/// </summary>
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
};
/// <summary>
/// A callback that is overridden by PowerShell so that the current working path can be retrieved.
/// </summary>
private static PathDelegate _GetWorkingPath = () => Directory.GetCurrentDirectory();
/// <summary>
/// A callback that is overridden by PowerShell so that the current working path can be retrieved.
/// </summary>
private static PathDelegate _GetWorkingPath = () => Directory.GetCurrentDirectory();
public PSRuleOption()
public PSRuleOption()
{
// Set defaults
Configuration = new ConfigurationOption();
Output = new OutputOption();
}
public ConfigurationOption Configuration { get; set; }
/// <summary>
/// Options that affect how output is generated.
/// </summary>
public OutputOption Output { get; set; }
/// <summary>
/// Set working path from PowerShell host environment.
/// </summary>
/// <param name="executionContext">An $ExecutionContext object.</param>
/// <remarks>
/// Called from PowerShell.
/// </remarks>
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();
}
/// <summary>
/// Options that affect how output is generated.
/// </summary>
public OutputOption Output { get; set; }
/// <summary>
/// Get a full path instead of a relative path that may be passed from PowerShell.
/// </summary>
internal static string GetRootedPath(string path)
{
return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(GetWorkingPath(), path));
}
/// <summary>
/// Set working path from PowerShell host environment.
/// </summary>
/// <param name="executionContext">An $ExecutionContext object.</param>
/// <remarks>
/// Called from PowerShell.
/// </remarks>
public static void UseExecutionContext(EngineIntrinsics executionContext)
{
if (executionContext == null)
{
_GetWorkingPath = () => Directory.GetCurrentDirectory();
return;
}
_GetWorkingPath = () => executionContext.SessionState.Path.CurrentFileSystemLocation.Path;
}
/// <summary>
/// Get a full path instead of a relative path that may be passed from PowerShell.
/// </summary>
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);
}
/// <summary>
/// Get a full path instead of a relative path that may be passed from PowerShell.
/// </summary>
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;
/// <summary>
/// Get a full path instead of a relative path that may be passed from PowerShell.
/// </summary>
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;
}
}

Просмотреть файл

@ -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<string> 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<BranchStatus> 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<string> 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<string> 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<BranchStatus> 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<string> 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; }
}

Просмотреть файл

@ -5,118 +5,117 @@ using System;
using System.Runtime.Serialization;
using System.Security.Permissions;
namespace PSRule.Rules.GitHub.Pipeline
namespace PSRule.Rules.GitHub.Pipeline;
/// <summary>
/// A base class for all pipeline exceptions.
/// </summary>
public abstract class PipelineException : Exception
{
/// <summary>
/// A base class for all pipeline exceptions.
/// Creates a pipeline exception.
/// </summary>
public abstract class PipelineException : Exception
protected PipelineException()
{
/// <summary>
/// Creates a pipeline exception.
/// </summary>
protected PipelineException()
{
}
/// <summary>
/// Creates a pipeline exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
protected PipelineException(string message)
: base(message) { }
/// <summary>
/// Creates a pipeline exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
/// <param name="innerException">A nested exception that caused the issue.</param>
protected PipelineException(string message, Exception innerException)
: base(message, innerException) { }
protected PipelineException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
/// <summary>
/// A serialization exception.
/// Creates a pipeline exception.
/// </summary>
[Serializable]
public sealed class PipelineSerializationException : PipelineException
/// <param name="message">The detail of the exception.</param>
protected PipelineException(string message)
: base(message) { }
/// <summary>
/// Creates a pipeline exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
/// <param name="innerException">A nested exception that caused the issue.</param>
protected PipelineException(string message, Exception innerException)
: base(message, innerException) { }
protected PipelineException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
/// <summary>
/// A serialization exception.
/// </summary>
[Serializable]
public sealed class PipelineSerializationException : PipelineException
{
/// <summary>
/// Creates a serialization exception.
/// </summary>
public PipelineSerializationException()
{
/// <summary>
/// Creates a serialization exception.
/// </summary>
public PipelineSerializationException()
{
}
/// <summary>
/// Creates a serialization exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
public PipelineSerializationException(string message)
: base(message) { }
/// <summary>
/// Creates a serialization exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
/// <param name="innerException">A nested exception that caused the issue.</param>
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);
}
}
/// <summary>
/// An exception related to template linking.
/// Creates a serialization exception.
/// </summary>
[Serializable]
public sealed class InvalidTemplateLinkException : PipelineException
/// <param name="message">The detail of the exception.</param>
public PipelineSerializationException(string message)
: base(message) { }
/// <summary>
/// Creates a serialization exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
/// <param name="innerException">A nested exception that caused the issue.</param>
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)
{
/// <summary>
/// Creates a template linking exception.
/// </summary>
public InvalidTemplateLinkException()
{
}
if (info == null)
throw new ArgumentNullException(nameof(info));
/// <summary>
/// Creates a template linking exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
public InvalidTemplateLinkException(string message)
: base(message) { }
/// <summary>
/// Creates a template linking exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
/// <param name="innerException">A nested exception that caused the issue.</param>
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);
}
}
/// <summary>
/// An exception related to template linking.
/// </summary>
[Serializable]
public sealed class InvalidTemplateLinkException : PipelineException
{
/// <summary>
/// Creates a template linking exception.
/// </summary>
public InvalidTemplateLinkException()
{
}
/// <summary>
/// Creates a template linking exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
public InvalidTemplateLinkException(string message)
: base(message) { }
/// <summary>
/// Creates a template linking exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
/// <param name="innerException">A nested exception that caused the issue.</param>
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);
}
}

Просмотреть файл

@ -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);
}
/// <summary>
/// Get the character encoding for the specified output encoding.
/// </summary>
/// <param name="encoding"></param>
/// <returns></returns>
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);
}
/// <summary>
/// Get the character encoding for the specified output encoding.
/// </summary>
/// <param name="encoding"></param>
/// <returns></returns>
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);
}
}

Просмотреть файл

@ -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<BranchStatus> GetBranchStatus(string owner, string name, string branch)
{
var result = new List<BranchStatus>();
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)
/// <summary>
/// Get matching repositories for the GitHub organization.
/// </summary>
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<Data.CommunityProfile>($"https://api.github.com/repos/{owner}/{name}/community/profile", headers: _GITHUB_HEADERS_COMMUNITY_PROFILE);
return profile;
}
/// <summary>
/// Get branches for the repository.
/// </summary>
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<BranchStatus> GetBranchStatus(string owner, string name, string branch)
{
var result = new List<BranchStatus>();
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;
}
/// <summary>
/// Get matching repositories for the GitHub organization.
/// </summary>
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<Data.CommunityProfile>($"https://api.github.com/repos/{owner}/{name}/community/profile", headers: GITHUB_HEADERS_COMMUNITY_PROFILE);
return profile;
}
/// <summary>
/// Get branches for the repository.
/// </summary>
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<Octokit.CheckRun> 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;
}
}
/// <summary>
/// Get issue milestones for the repository.
/// </summary>
private Octokit.Milestone[] GetMilestonesInternal(string owner, string name)
{
var task = _Client.Issue.Milestone.GetAllForRepository(owner, name);
task.Wait();
return task.Result.ToArray();
}
/// <summary>
/// Get releases for the repository.
/// </summary>
private Octokit.Release[] GetReleasesInternal(string owner, string name)
{
var task = _Client.Repository.Release.GetAll(owner, name);
task.Wait();
return task.Result.ToArray();
}
/// <summary>
/// Get issue labels for the repository.
/// </summary>
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<string>();
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<IReadOnlyList<Octokit.RepositoryContent>>[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<Octokit.RepositoryContent>();
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<string> 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();
}
/// <summary>
/// Do a lookup against a login to determine if it is an organization.
/// </summary>
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<Octokit.CheckRun> 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;
}
}
/// <summary>
/// Get issue milestones for the repository.
/// </summary>
private Octokit.Milestone[] GetMilestonesInternal(string owner, string name)
{
var task = _Client.Issue.Milestone.GetAllForRepository(owner, name);
task.Wait();
return task.Result.ToArray();
}
/// <summary>
/// Get releases for the repository.
/// </summary>
private Octokit.Release[] GetReleasesInternal(string owner, string name)
{
var task = _Client.Repository.Release.GetAll(owner, name);
task.Wait();
return task.Result.ToArray();
}
/// <summary>
/// Get issue labels for the repository.
/// </summary>
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<string>();
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<IReadOnlyList<Octokit.RepositoryContent>>[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<Octokit.RepositoryContent>();
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<string> 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();
}
/// <summary>
/// Do a lookup against a login to determine if it is an organization.
/// </summary>
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
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -1,13 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace PSRule.Rules.GitHub.Pipeline
{
/// <summary>
/// Extensions for logging to the pipeline.
/// </summary>
internal static class LoggingExtensions
{
namespace PSRule.Rules.GitHub.Pipeline;
/// <summary>
/// Extensions for logging to the pipeline.
/// </summary>
internal static class LoggingExtensions
{
}
}

Просмотреть файл

@ -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;
/// <summary>
/// An output writer that writes output to disk.
/// </summary>
internal sealed class FileOutputWriter : PipelineWriter
{
/// <summary>
/// An output writer that writes output to disk.
/// </summary>
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);
}
}
}

Просмотреть файл

@ -4,21 +4,20 @@
using Newtonsoft.Json;
using PSRule.Rules.GitHub.Configuration;
namespace PSRule.Rules.GitHub.Pipeline.Output
{
internal sealed class JsonOutputWriter : SerializationOutputWriter<object>
{
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<object>
{
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);
}
}

Просмотреть файл

@ -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;
/// <summary>
/// An output writer that returns output to the host PowerShell runspace.
/// </summary>
internal sealed class PSPipelineWriter : PipelineWriter
{
/// <summary>
/// An output writer that returns output to the host PowerShell runspace.
/// </summary>
internal sealed class PSPipelineWriter : PipelineWriter
private const string Source = "PSRule";
private const string HostTag = "PSHOST";
private Action<string> _OnWriteWarning;
private Action<string> _OnWriteVerbose;
private Action<ErrorRecord> _OnWriteError;
private Action<InformationRecord> _OnWriteInformation;
private Action<string> _OnWriteDebug;
internal Action<object, bool> 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<string> OnWriteWarning;
private Action<string> OnWriteVerbose;
private Action<ErrorRecord> OnWriteError;
private Action<InformationRecord> OnWriteInformation;
private Action<string> OnWriteDebug;
internal Action<object, bool> 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
/// <summary>
/// Core methods to hand off to logger.
/// </summary>
/// <param name="errorRecord">A valid PowerShell error record.</param>
public override void WriteError(ErrorRecord errorRecord)
{
if (OnWriteError == null || !ShouldWriteError())
return;
OnWriteError(errorRecord);
}
/// <summary>
/// Core method to hand off verbose messages to logger.
/// </summary>
/// <param name="message">A message to log.</param>
public override void WriteVerbose(string message)
{
if (OnWriteVerbose == null || !ShouldWriteVerbose())
return;
OnWriteVerbose(message);
}
/// <summary>
/// Core method to hand off warning messages to logger.
/// </summary>
/// <param name="message">A message to log</param>
public override void WriteWarning(string message)
{
if (OnWriteWarning == null || !ShouldWriteWarning())
return;
OnWriteWarning(message);
}
/// <summary>
/// Core method to hand off information messages to logger.
/// </summary>
public override void WriteInformation(InformationRecord informationRecord)
{
if (OnWriteInformation == null || !ShouldWriteInformation())
return;
OnWriteInformation(informationRecord);
}
/// <summary>
/// Core method to hand off debug messages to logger.
/// </summary>
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
/// <summary>
/// Core methods to hand off to logger.
/// </summary>
/// <param name="errorRecord">A valid PowerShell error record.</param>
public override void WriteError(ErrorRecord errorRecord)
{
if (_OnWriteError == null || !ShouldWriteError())
return;
_OnWriteError(errorRecord);
}
/// <summary>
/// Core method to hand off verbose messages to logger.
/// </summary>
/// <param name="message">A message to log.</param>
public override void WriteVerbose(string message)
{
if (_OnWriteVerbose == null || !ShouldWriteVerbose())
return;
_OnWriteVerbose(message);
}
/// <summary>
/// Core method to hand off warning messages to logger.
/// </summary>
/// <param name="message">A message to log</param>
public override void WriteWarning(string message)
{
if (_OnWriteWarning == null || !ShouldWriteWarning())
return;
_OnWriteWarning(message);
}
/// <summary>
/// Core method to hand off information messages to logger.
/// </summary>
public override void WriteInformation(InformationRecord informationRecord)
{
if (_OnWriteInformation == null || !ShouldWriteInformation())
return;
_OnWriteInformation(informationRecord);
}
/// <summary>
/// Core method to hand off debug messages to logger.
/// </summary>
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
}

Просмотреть файл

@ -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);
/// <summary>
/// A helper class for building a pipeline from PowerShell.
/// </summary>
public static class PipelineBuilder
{
internal delegate bool ShouldProcess(string target, string action);
/// <summary>
/// A helper class for building a pipeline from PowerShell.
/// </summary>
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
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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<T> : PipelineWriter
public virtual void WriteObject(object sendToPipeline, bool enumerateCollection)
{
private readonly List<T> _Result;
if (_Writer == null || sendToPipeline == null)
return;
protected SerializationOutputWriter(PipelineWriter inner, PSRuleOption option)
: base(inner, option)
{
_Result = new List<T>();
}
_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<T> : PipelineWriter
{
private readonly List<T> _Result;
protected SerializationOutputWriter(PipelineWriter inner, PSRuleOption option)
: base(inner, option)
{
_Result = new List<T>();
}
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);
}

Просмотреть файл

@ -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<PSObject>();
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<PSObject>();
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();
}
}

Просмотреть файл

@ -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;
/// <summary>
/// External helper to be referenced within rules.
/// </summary>
public static class Helper
{
/// <summary>
/// External helper to be referenced within rules.
/// </summary>
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<PSObject>();
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<PSObject>();
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);
}
}

Просмотреть файл

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<ProjectGuid>{9ff459f0-c7bc-4936-9fc3-4d92b30b02a1}</ProjectGuid>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
@ -11,6 +11,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.1" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.3.7" />
<PackageReference Include="System.Management.Automation" Version="7.3.7" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>