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
19 changes: 19 additions & 0 deletions src/docs/Coding-Standards/GitHub-Actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ this section is the Actions-specific view of it.
level, since the workflow and its one job are equivalent.)
- **Declare permissions per job**, not only at workflow level, and grant only
the scopes that job needs. Most jobs need no more than `contents: read`.
- **Disable checkout's persisted credentials unless the job intentionally pushes.** `actions/checkout` writes the token into the local Git configuration by default; set `persist-credentials: false` for read-only jobs so later steps cannot accidentally push with it. When a job must push, isolate that work in the smallest possible job, grant the write scope there, and make the persisted credential an explicit review point.

```yaml
# Default-deny floor at the top; each job opts into exactly what it needs.
Expand Down Expand Up @@ -145,6 +146,14 @@ directly into a `run:` script allows shell injection. **Pass untrusted values
through an `env:` variable** and reference the variable, which is not re-parsed
as script.

Avoid `pull_request_target` and `workflow_run` unless the workflow genuinely
needs the elevated context. Both can connect untrusted contributions to tokens,
secrets, or artifacts from a more privileged run. If one is required, split the
workflow so untrusted code is never executed with a writable token: validate or
build the contributor's code in the unprivileged workflow, then let the
privileged workflow consume only trusted, explicitly produced artifacts or
metadata.

```yaml
# Correct — value arrives as an environment variable, not inlined into the shell
- name: Print the PR title
Expand Down Expand Up @@ -217,6 +226,8 @@ jobs:
# Sequential, state-sharing work belongs in one job as ordered steps.
- name: Check out the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Build
shell: pwsh
Expand Down Expand Up @@ -658,6 +669,14 @@ Both run through [super-linter](https://github.com/super-linter/super-linter) on
every push and pull request, so a workflow that breaks this standard fails the
build.

Treat `actionlint` errors and high-severity `zizmor` findings as blockers.
Medium-severity `zizmor` findings are fixed unless the workflow documents why
the risk is accepted; low-severity findings are fixed when the simpler, safer
shape is available. In practice, an audit starts by checking for unpinned
actions, broad or inherited secrets, missing `permissions`, persisted checkout
credentials, untrusted context interpolation, and elevated triggers such as
`pull_request_target` or `workflow_run`.

```text
# Single-file action — main.<ext> at the action root
.github/actions/link-check/
Expand Down
1 change: 1 addition & 0 deletions src/docs/Coding-Standards/PowerShell/Functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Send each kind of message to the stream built for it, so a caller can capture, r
- **Results** are objects on the output stream — emit them implicitly by naming the object on its own line; do **not** use `return $obj` to emit, and in a pipeline function emit from `process`, not `end`.
- **Emit one object type**, matching `[OutputType()]`.
- **`Write-Verbose`** for status a caller may want (`-Verbose`), **`Write-Debug`** for maintainer breadcrumbs (`-Debug`), and **`Write-Progress`** for progress that need not persist.
- **Do not pass `-Verbose` or `-Debug` explicitly to internal calls** just because the current function was invoked with them. Common parameters are caller controls; write to the stream in the current function and let the caller decide which streams to show or capture.
- **`Write-Warning`** and **`Write-Error`** for warnings and non-terminating errors.
- **`Write-Host`** only for `Show-` or `Format-` verbs or an interactive prompt — never for data another command might consume.

Expand Down
5 changes: 5 additions & 0 deletions src/docs/Coding-Standards/PowerShell/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ These hold for all PowerShell, whatever the construct:
- **`PascalCase`** for functions, parameters, public variables, and class members; `camelCase` for local variables.
- **Full cmdlet names, never aliases** (`Where-Object`, not `?`; `ForEach-Object`, not `%`).
- **Full parameter names, and standard ones.** Pass parameters by name and avoid positional arguments in shared code — `Get-Process -Name pwsh`, not `Get-Process pwsh` — so a call survives parameter-set changes and reads clearly. Name your own parameters after PowerShell's built-ins (`Path`, `Name`, `ComputerName`), not `$Param_Computer`.
- **Map data and integration verbs predictably.** Data modules use `ConvertFrom-` / `ConvertTo-` around a neutral PowerShell object shape, `Import-` / `Export-` for file or store round trips, and `Format-`, `Merge-`, `Compare-`, `Test-`, or `Remove-<Noun>Entry` for structure operations. API and integration modules name commands after the resource and intent, not the transport: map `GET` to `Get-`, create-style `POST` to `New-` / `Add-`, `PUT` / `PATCH` to `Set-` / `Update-`, `DELETE` to `Remove-`, and non-CRUD operations to the approved verb that matches the user intent.
- **Set `$ErrorActionPreference = 'Stop'`** at the top of every script and module so errors are terminating, not silently swallowed.
- **Emit objects, not formatted text.** Return rich objects and let the caller format; reserve `Write-Host` for genuine console UX, and use `Write-Verbose` / `Write-Information` for progress narration.

Expand All @@ -49,8 +50,10 @@ These rules define the layout; [PSScriptAnalyzer](#toolchain) enforces them (its
- **Indent with four spaces, never tabs**, and indent comment-based help to align with the function it documents.
- **One space around operators and after commas** (`$a -eq $b`, `@(1, 2, 3)`), and **one space between a type and the name** — `[string] $Name`, not `[string]$Name`.
- **`elseif` is one word**, not `else if`.
- **Use lowercase language keywords** (`if`, `else`, `foreach`, `class`, `enum`, `return`) and reserve PascalCase for commands and named symbols.
- **Blank lines separate logical blocks.** No trailing whitespace, and end every file with a single newline.
- **Keep code lines readable — aim for roughly 120 columns.** When a call grows long, prefer [splatting](#idioms-and-pitfalls) over backtick line-continuations.
- **Avoid semicolons, ternary expressions, and regions in shared code.** Put one statement on each line, use ordinary `if` / `else` when a conditional value matters, and structure code with functions, modules, and headings rather than `#region` blocks.

## Idioms and pitfalls

Expand All @@ -60,13 +63,15 @@ Beyond the basics, these language-specific habits keep PowerShell correct and fa
- **Splat calls that carry many parameters.** Build a `@{}` of parameters and splat it (`Get-Thing @params`) instead of a long line of `-Param value` pairs or backtick continuations; it reads better and diffs cleanly.
- **Put `$null` on the left of a comparison** — `$null -eq $x`, never `$x -eq $null`. Against a collection the right-hand form *filters* rather than tests. Use `-contains` / `-in` for membership, never `-eq`.
- **Match text with the operator built for it.** Use `-like` for wildcard patterns and `-match` for regular expressions instead of hand-rolled string surgery; both default to case-insensitive, so add the `-c` prefix (`-clike`, `-cmatch`, `-ceq`) when a comparison must be case-sensitive.
- **Use the built-in intent checks for strings and wildcards.** Use `[string]::IsNullOrWhiteSpace($value)` for blank input and `[System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($pattern)` when deciding whether a value contains wildcard syntax.
- **Reuse before you build.** Work down the [reuse order](../Functions.md#reuse-before-you-build) — a built-in cmdlet or operator, then an existing function (public or private), then a trusted module (`#Requires -Modules` / `RequiredModules`), then your own code (small logic inline, a larger capability as its own module). State a dependency's acceptable versions as a [version range](Version-Constraints.md).
- **PowerShell already *is* .NET; work at that level rather than wrapping it.** Casts, type accelerators (`[datetime]`, `[int]`), the `-split` / `-replace` / `-match` operators, and member methods (`.Trim()`, `.Where()`) all resolve to the base class library — using .NET means reaching for BCL types and methods for the computation, not restating everything as `[Namespace.Type]::Method(...)`. Where idiomatic PowerShell already resolves to the same .NET call, leave it; reach for explicit .NET only where it is measurably faster or more precise, and keep cmdlets and the pipeline where you need them for glue or readability.
- **Do the work in .NET when you implement it.** When you write the logic yourself — or fix an internal function that is too slow or imprecise on a hot path — call the .NET base class library directly instead of a cmdlet pipeline: `[System.IO.File]::ReadAllText($path)` over `Get-Content -Raw`, `[System.IO.Path]::Combine(...)` for paths, `[System.Text.StringBuilder]` for repeated concatenation, `[int]::TryParse(...)` for parsing. .NET methods are faster and their contracts are precise; keep cmdlets where their clarity is worth more than the speed. The next two rules are specific cases.
- **Suppress unwanted output with `$null = ...`** (or `[void]` for method calls), not `| Out-Null` — the pipeline form is markedly slower on hot paths.
- **Build collections with a typed list, not `+=` in a loop.** `$a += $x` reallocates the whole array every iteration; use `[System.Collections.Generic.List[T]]` with `.Add()`, and prefer a cmdlet's `-Filter` over piping to `Where-Object` on large sets.
- **Guard a value that must not change.** Declare it with `Set-Variable -Name Pi -Value 3.14159 -Option ReadOnly` — or `-Option Constant` for one that can never be reassigned or removed — so an accidental write fails loudly instead of quietly winning.
- **Keep secrets out of source, and never `Invoke-Expression` untrusted input.** Accept credentials as a `[PSCredential]` parameter with the `[Credential()]` attribute rather than calling `Get-Credential` inside a reusable function, so a caller can pass one they already hold, and take other sensitive values as `[securestring]`. Guard state-changing commands with `ShouldProcess` (see [Functions](Functions.md)); the wider rules live in the [Security](../Security.md) baseline.
- **Read cross-platform environment values through .NET when the provider is not the point.** Prefer `[Environment]::GetEnvironmentVariable('NAME')` for process environment reads that should behave the same on every platform; use `$env:NAME` when you are intentionally working through the PowerShell environment provider.

## Toolchain

Expand Down