Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ What exists today, and the work package that adds the rest. A folder is created
| `.github/ISSUE_TEMPLATE/` | Issue forms: work package, task or spin-off. | written |
| `PSScriptAnalyzerSettings.psd1` | Analyzer settings: errors and warnings, default rules, justified exclusions only. | WP00 |
| `schemas/` | JSON schemas for every file in an organization rulebook repository, draft 2020-12, no `$id`; names and URLs in [docs/reference/naming.md](docs/reference/naming.md). | WP02 ([#4](https://github.com/ALCops/rulebook-engine/issues/4)) |
| `tests/` | `Smoke.Tests.ps1`, `Schemas.Tests.ps1`, `Rulebook.Generate.Tests.ps1`, `Rulebook.Validate.Tests.ps1`, `Validate.Action.Tests.ps1`, `Rulebook.Template.Tests.ps1`, `Rulebook.Publish.Tests.ps1`, `Publish.Action.Tests.ps1`, `Get-RulebookSkeletons.Tests.ps1`, `Rulebook.GitHub.Tests.ps1`, `Rulebook.Update.Tests.ps1`, `CheckForUpdates.Action.Tests.ps1`, `Rulebook.NuGet.Tests.ps1`, `Rulebook.Extract.Tests.ps1`, `Rulebook.Catalog.Tests.ps1`, `Rulebook.Quarantine.Tests.ps1`, `Rulebook.Scan.Tests.ps1` and `ScanDiagnostics.Action.Tests.ps1` now; later one suite per module and action, with fixtures under `tests/fixtures/`. | WP00, WP02, WP03, WP04, WP05, WP06, WP07, WP08; WP12 ([#14](https://github.com/ALCops/rulebook-engine/issues/14)) and every module work package |
| `tests/` | `Smoke.Tests.ps1`, `Schemas.Tests.ps1`, `Rulebook.Generate.Tests.ps1`, `Rulebook.Validate.Tests.ps1`, `Validate.Action.Tests.ps1`, `Rulebook.Template.Tests.ps1`, `Rulebook.Publish.Tests.ps1`, `Publish.Action.Tests.ps1`, `Get-RulebookSkeletons.Tests.ps1`, `Rulebook.GitHub.Tests.ps1`, `Rulebook.Update.Tests.ps1`, `CheckForUpdates.Action.Tests.ps1`, `Rulebook.NuGet.Tests.ps1`, `Rulebook.Extract.Tests.ps1`, `Rulebook.Catalog.Tests.ps1`, `Rulebook.Quarantine.Tests.ps1`, `Rulebook.Scan.Tests.ps1`, `ScanDiagnostics.Action.Tests.ps1` and `Rulebook.Action.Tests.ps1` now; later one suite per module and action, with fixtures under `tests/fixtures/`. | WP00, WP02, WP03, WP04, WP05, WP06, WP07, WP08; WP12 ([#14](https://github.com/ALCops/rulebook-engine/issues/14)) and every module work package |
| `tests/fixtures/schemas/` | One file per case: `<valid\|invalid>/<schema-basename>/<reason>.json`, each invalid file a one-change mutation of a valid one. | WP02 |
| `tests/fixtures/repos/` | Organization rulebook repositories for the module suites. `valid-minimal` (30-id catalog, four levels, three stages), `stale-endpoints` and `update-org` (WP07) are complete on disk, their `rulesets/` written by `Update-RulebookEndpoints`; every other folder is an overlay holding only the files it changes, copied over `valid-minimal` by `New-FixtureRepo`. | WP03 |
| `tests/fixtures/templates/` | `v1` and `v2`: two versions of a mini template for the update suites, with the exact difference list and the derivation of `repos/update-org` in its README. | WP07 |
| `tests/fixtures/stub-analyzers/` | C# stubs of the compiler and the twelve cop assemblies and `Build-StubPackage.ps1`, which compiles them with the Roslyn of pwsh into fixture nupkgs (variants tools 18.0.43.1464 and 30.0.42.60748-beta, ALCops 1.3.1, 1.4.0-beta.1 and 1.4.0) for the scan suites; `tests/Helpers/StubFeed.ps1` builds them once per source hash. | WP08 |
| `tests/fixtures/matrix/` | `tiny`: a six-id level content in the layout of `docs/rulebook/` (inventory, matrix, resolved cells, levels, stages, twins) for the unit cases of the Template suite. | WP04 |
| `tests/Helpers/` | Helpers the suites dot-source in `BeforeAll`: `RepoFixture.ps1` copies a fixture into `TestDrive`, edits its JSON, creates git repositories, bare origins with a push-refusing hook and the synthetic performance rulebook; `StubFeed.ps1` builds NuGet flat containers of the stub analyzer packages. | WP03, WP08 |
| `modules/` | PowerShell modules shared by the actions, each a `.psm1` with a `.psd1` manifest: `Rulebook.Generate`, `Rulebook.Validate`, `Rulebook.Template`, `Rulebook.Publish`, `Rulebook.GitHub`, `Rulebook.Update`, `Rulebook.NuGet`, `Rulebook.Extract`, `Rulebook.Catalog`, `Rulebook.Quarantine` and `Rulebook.Scan` (written). | WP03 to WP09 |
| `modules/` | PowerShell modules shared by the actions, each a `.psm1` with a `.psd1` manifest: `Rulebook.Generate`, `Rulebook.Validate`, `Rulebook.Template`, `Rulebook.Publish`, `Rulebook.GitHub`, `Rulebook.Update`, `Rulebook.NuGet`, `Rulebook.Extract`, `Rulebook.Catalog`, `Rulebook.Quarantine`, `Rulebook.Scan` and `Rulebook.Action` (written). | WP03 to WP09 |
| `actions/` | Composite actions, one folder each with `action.yaml` and its entry script. | WP03 to WP09 |
| `actions/Validate/` | `action.yaml` and `Validate.ps1`: checks C1 to C16, annotations, the job summary with the effective diff ([ARCHITECTURE.md](docs/ARCHITECTURE.md) section 5.5). | WP03 |
| `actions/Publish/` | `action.yaml` and `Publish.ps1`: stage the endpoints, the rendered skeletons and `index.html`, the Pages preflight, deploy to GitHub Pages, the reachability check ([ARCHITECTURE.md](docs/ARCHITECTURE.md) section 7.2, [docs/reference/publish-targets.md](docs/reference/publish-targets.md)). | WP05 |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The engine behind [ALCops/rulebook](https://github.com/ALCops/rulebook): the com
| Path | Content | Status |
|---|---|---|
| `actions/<Name>/action.yaml` | Composite actions, each running a PowerShell 7 entry script on `ubuntu-latest`: `Validate` (checks, effective diff, update check), `Publish` (gate, stage, deploy, verify), `CheckForUpdates` (update from the template), `ScanDiagnostics` (the daily diagnostic scan). | `Validate` (WP03), `Publish` (WP05), `CheckForUpdates` (WP07) and `ScanDiagnostics` (WP08) written; `ChangeRule` planned (WP09) |
| `modules/Rulebook.*.psm1` | PowerShell modules shared by the actions, each with a `.psd1` manifest: `Rulebook.Generate` (level chain + stage deltas + twins setting + overrides + quarantine to sparse flat endpoints, effective diff), `Rulebook.Validate` (checks C1 to C16), `Rulebook.Template` (level, stage, twins, catalog and skeleton files of `template/` from `docs/rulebook/`), `Rulebook.Publish` (staging, Pages deploy, reachability), `Rulebook.Update` and `Rulebook.GitHub` (the update and the GitHub plumbing), and for the scan `Rulebook.NuGet`, `Rulebook.Extract`, `Rulebook.Catalog`, `Rulebook.Quarantine` and `Rulebook.Scan`. | written (WP03 to WP08) |
| `modules/Rulebook.*.psm1` | PowerShell modules shared by the actions, each with a `.psd1` manifest: `Rulebook.Generate` (level chain + stage deltas + twins setting + overrides + quarantine to sparse flat endpoints, effective diff), `Rulebook.Validate` (checks C1 to C16), `Rulebook.Template` (level, stage, twins, catalog and skeleton files of `template/` from `docs/rulebook/`), `Rulebook.Publish` (staging, Pages deploy, reachability), `Rulebook.Update` and `Rulebook.GitHub` (the update and the GitHub plumbing), for the scan `Rulebook.NuGet`, `Rulebook.Extract`, `Rulebook.Catalog`, `Rulebook.Quarantine` and `Rulebook.Scan`, and `Rulebook.Action` (the helpers every action entry script shares: annotations, the failure kind, the job summary and `GITHUB_OUTPUT`). | written (WP03 to WP09) |
| `template/` | Source of the template content that a deploy workflow copies into `ALCops/rulebook`, pinning action references from `@main` to `@v1`. Its `base/`, `stages/` and `rulesets/` are generated from `docs/rulebook/`. | written (WP03 to WP08), 43 files: settings, the workflows `Validate.yaml`, `Publish.yaml`, `UpdateRulebookSystemFiles.yaml` and `ScanDiagnostics.yaml`, the README files, empty overrides and quarantine files, and the 32 generated files; the other workflows come with their work packages, the deploy with WP13 |
| `schemas/` | JSON schemas for every file in an organization rulebook repository (ruleset profiles, overrides, quarantine, twins, catalog, scan state, settings), served from the `v1` branch over raw URLs, which go live with WP13 ([#15](https://github.com/ALCops/rulebook-engine/issues/15)) and return 404 until then; the tests use the local files. See [docs/reference/naming.md](docs/reference/naming.md). | written (WP02) |
| `tests/` | Pester 6 suites, one per module and action, with fixtures under `tests/fixtures/`: the schema suite and its fixtures under `tests/fixtures/schemas/`; the Generate, Validate and Validate action suites with organization rulebook fixtures under `tests/fixtures/repos/` and helpers in `tests/Helpers/`. | a suite per module and action (WP00 to WP08); the scan suites build stub analyzer packages from `tests/fixtures/stub-analyzers/` at test time |
Expand Down
84 changes: 21 additions & 63 deletions actions/CheckForUpdates/CheckForUpdates.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -52,57 +52,17 @@ $modules = Join-Path $PSScriptRoot '..' '..' 'modules'
Import-Module (Join-Path $modules 'Rulebook.Generate.psd1') -Force
Import-Module (Join-Path $modules 'Rulebook.GitHub.psd1') -Force
Import-Module (Join-Path $modules 'Rulebook.Update.psd1') -Force
Import-Module (Join-Path $modules 'Rulebook.Action.psd1') -Force

$docsUrl = 'https://github.com/ALCops/rulebook/blob/main/docs/ghtokenworkflow.md'

function Format-AnnotationText {
# Workflow command escaping: the message part escapes %, CR and LF; a property value also : and ,.
param([AllowNull()][string]$Text, [switch]$Property)
if ($null -eq $Text) { return '' }
$escaped = $Text.Replace('%', '%25').Replace("`r", '%0D').Replace("`n", '%0A')
if ($Property) { $escaped = $escaped.Replace(':', '%3A').Replace(',', '%2C') }
return $escaped
}

function ConvertTo-SingleLine {
# A message on one Markdown line (list item or paragraph); no table escaping.
param([AllowNull()][string]$Text)
if ([string]::IsNullOrEmpty($Text)) { return '' }
return $Text.Replace("`r", ' ').Replace("`n", ' ')
}

function Add-Annotation {
param([ValidateSet('error', 'warning', 'notice')][string]$Command = 'error', [string]$File, [string]$Title = 'CheckForUpdates', [Parameter(Mandatory)][string]$Message)
$properties = "title=$(Format-AnnotationText $Title -Property)"
if ($File) { $properties = "file=$(Format-AnnotationText $File -Property),$properties" }
$line = "::$Command $properties::$(Format-AnnotationText $Message)"
$script:annotations.Add($line)
if ($Command -eq 'error') { $script:errorMessages.Add($Message) }
Write-Host $line
}

function Write-Text {
param([AllowNull()][string]$Path, [Parameter(Mandatory)][AllowEmptyString()][string]$Text)
if ([string]::IsNullOrEmpty($Path)) { return }
[System.IO.File]::AppendAllText($Path, $Text.Replace("`r`n", "`n"), [System.Text.UTF8Encoding]::new($false))
}

# [System.IO.File] resolves a relative path against the process directory, not the PowerShell location.
$resolvePath = { param($Path) if ([string]::IsNullOrEmpty($Path)) { $Path } else { $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) } }
$SummaryPath = & $resolvePath $SummaryPath
$ownWork = [string]::IsNullOrEmpty($WorkPath)
if ($ownWork) {
$tempRoot = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { [System.IO.Path]::GetTempPath() }
$WorkPath = Join-Path $tempRoot ('rulebook-update-' + [guid]::NewGuid().ToString('n').Substring(0, 8))
}
$WorkPath = & $resolvePath $WorkPath
$script:annotations = [System.Collections.Generic.List[string]]::new()
$script:errorMessages = [System.Collections.Generic.List[string]]::new()
$script:failure = $null
function Add-Failure {
param([Parameter(Mandatory)][string]$Kind)
if ($null -eq $script:failure) { $script:failure = $Kind }
}
$WorkPath = Resolve-ActionPath $WorkPath
$ctx = New-ActionContext -Title 'CheckForUpdates'

# Every token obtained by an exchange is masked before anything can print it.
$maskToken = { param([string]$Value) if (-not [string]::IsNullOrEmpty($Value)) { Write-Host "::add-mask::$Value" } }
Expand Down Expand Up @@ -133,11 +93,11 @@ try {
# 1. Template URL and the token guard (before any request).
$requested = if (-not [string]::IsNullOrWhiteSpace($TemplateUrl)) { $TemplateUrl } else { [string](& $setting 'templateUrl') }
if ($Update -and -not [string]::IsNullOrEmpty($name) -and -not $nameValid) {
Add-Failure 'token'
Add-Failure -Context $ctx -Kind 'token'
throw "ghTokenWorkflowSecretName '$name' in .github/Rulebook-Settings.json is not a valid secret name (letters, digits and underscores, not starting with a digit or GITHUB_). Read $docsUrl"
}
if ($Update -and [string]::IsNullOrWhiteSpace($Token)) {
Add-Failure 'token'
Add-Failure -Context $ctx -Kind 'token'
throw "The $secretName secret is needed to update system files. Read $docsUrl"
}

Expand All @@ -149,7 +109,7 @@ try {
try {
$access = Get-GitHubAccessToken -Token $Token -Repository $Repository -ApiUrl $ApiUrl
} catch {
Add-Failure 'token'
Add-Failure -Context $ctx -Kind 'token'
throw "The $secretName secret could not be used: $($_.Exception.Message)"
}
if (-not [string]::IsNullOrEmpty($access.Token)) { & $maskToken $access.Token }
Expand Down Expand Up @@ -179,10 +139,10 @@ try {
$plan = Get-RulebookUpdatePlan -RepositoryRoot $root -Template $template -WorkPath $WorkPath
} catch {
if ($Update) {
if ($null -eq $template) { Add-Failure 'template' }
if ($null -eq $template) { Add-Failure -Context $ctx -Kind 'template' }
throw
}
Add-Annotation -Command warning -Message "update check skipped: $($_.Exception.Message)"
Add-Annotation -Context $ctx -Command warning -Message "update check skipped: $($_.Exception.Message)"
$summaryMessage = "update check skipped: $($_.Exception.Message)"
$plan = $null
}
Expand All @@ -194,17 +154,17 @@ try {
$updatesAvailable = $plan.Valid -and $plan.UpdatesAvailable

if (-not $Update) {
Add-Annotation -Command $status.Command -Message $status.Message
Add-Annotation -Context $ctx -Command $status.Command -Message $status.Message
$summaryMessage = $status.Message
} else {
# 4. Update mode.
if (-not $plan.Valid) {
foreach ($finding in @($plan.Findings | Where-Object Severity -EQ 'error')) {
$message = if ($finding.Id) { "$($finding.Id): $($finding.Message)" } else { $finding.Message }
$file = if ($finding.File) { [System.IO.Path]::GetRelativePath($workspace, (Join-Path $root $finding.File)).Replace($separator, '/') } else { $null }
Add-Annotation -File $file -Title $finding.Rule -Message "The updated rulebook would not validate: $message"
Add-Annotation -Context $ctx -File $file -Title $finding.Rule -Message "The updated rulebook would not validate: $message"
}
Add-Failure 'validation'
Add-Failure -Context $ctx -Kind 'validation'
$summaryMessage = 'The updated rulebook does not validate; nothing was pushed. The findings come from the repository after the update.'
$failed = $true
} else {
Expand All @@ -218,35 +178,35 @@ try {
} catch {
$stage = [string]$_.Exception.Data['Stage']
if ($stage -cnotin 'push', 'pull-request') { $stage = 'push' }
Add-Failure $stage
Add-Failure -Context $ctx -Kind $stage
$what = if ($stage -eq 'pull-request') { 'Failed to create the pull request for the Rulebook system files' } else { 'Failed to update the Rulebook system files' }
throw "$what. Make sure that the token in the secret $secretName is not expired and may write contents, pull requests and workflows of $Repository. Read $docsUrl (Error was: $($_.Exception.Message))"
}
switch ($publish.Result) {
'exists' {
Add-Annotation -Command warning -Message "Pull request already exists: $($publish.PullRequestUrl)"
Add-Annotation -Context $ctx -Command warning -Message "Pull request already exists: $($publish.PullRequestUrl)"
$summaryMessage = "Pull request already exists: $($publish.PullRequestUrl)"
}
'no-changes' {
Add-Annotation -Command notice -Message 'No updates available'
Add-Annotation -Context $ctx -Command notice -Message 'No updates available'
$summaryMessage = 'No updates available'
}
'direct-commit' {
Add-Annotation -Command notice -Message "Rulebook system files updated in $($publish.Branch) ($($publish.Sha))"
Add-Annotation -Context $ctx -Command notice -Message "Rulebook system files updated in $($publish.Branch) ($($publish.Sha))"
$summaryMessage = "Committed to $($publish.Branch)"
}
default {
$suffix = if ($publish.Fallback) { ' (the direct commit was refused)' } else { '' }
Add-Annotation -Command notice -Message "Pull request: $($publish.PullRequestUrl)$suffix"
Add-Annotation -Context $ctx -Command notice -Message "Pull request: $($publish.PullRequestUrl)$suffix"
$summaryMessage = "Pull request: $($publish.PullRequestUrl)$suffix"
}
}
}
}
}
} catch {
Add-Annotation -Message $_.Exception.Message
Add-Failure 'error'
Add-Annotation -Context $ctx -Message $_.Exception.Message
Add-Failure -Context $ctx -Kind 'error'
$summaryMessage = $_.Exception.Message
$failed = $true
} finally {
Expand All @@ -270,18 +230,16 @@ $summary = Limit-SummaryText -Text $summary -MaxBytes $SummaryLimit -Footer "The
Write-Text -Path $SummaryPath -Text $summary
$pullRequestUrl = if ($null -ne $publish -and $publish.PullRequestUrl) { $publish.PullRequestUrl } else { '' }
$sha = if ($null -ne $template) { $template.Sha } else { '' }
if ($env:GITHUB_OUTPUT) {
Write-Text -Path $env:GITHUB_OUTPUT -Text ("updatesAvailable={0}`npullRequestUrl={1}`ntemplateSha={2}`nfailure={3}`n" -f $updatesAvailable.ToString().ToLowerInvariant(), $pullRequestUrl, $sha, $script:failure)
}
Write-ActionOutput -Outputs ([ordered]@{ updatesAvailable = $updatesAvailable.ToString().ToLowerInvariant(); pullRequestUrl = $pullRequestUrl; templateSha = $sha; failure = $ctx.Failure })
[pscustomobject]@{
ExitCode = $(if ($failed) { 1 } else { 0 })
Mode = $mode
Failure = $script:failure
Failure = $ctx.Failure
UpdatesAvailable = $updatesAvailable
TemplateSha = $sha
PullRequestUrl = $pullRequestUrl
Plan = $plan
Result = $publish
Annotations = $script:annotations.ToArray()
Annotations = $ctx.Annotations.ToArray()
Summary = $summary
}
Loading
Loading