diff --git a/src/docs/Style-Guides/GitHub-Actions.md b/src/docs/Style-Guides/GitHub-Actions.md new file mode 100644 index 0000000..ea323a5 --- /dev/null +++ b/src/docs/Style-Guides/GitHub-Actions.md @@ -0,0 +1,232 @@ +--- +title: GitHub Actions +description: GitHub Actions style guidelines for consistency and security across workflows and composite actions. +--- + +# GitHub Actions style guidelines + +This document defines the GitHub Actions style guidelines for all workflow files (`.github/workflows/*.yml`), composite actions (`action.yml`), and reusable workflows in PSModule repositories. These rules follow GitHub Actions best practices and the security guidance enforced by [zizmor](https://github.com/woodruffw/zizmor). + +## Scope + +- Applies to `.github/workflows/*.{yml,yaml}`, `action.yml`, `.github/dependabot.yml`, and `CODEOWNERS` entries for workflows. +- Application source code stays out of scope. If a fix requires changes outside `.github/`, surface it as a recommendation instead. + +## Naming + +- Give every job and every step a `name:` field +- Use short, human-readable names that describe what the job or step does, not how +- Keep naming consistent with existing workflows in the repository + +**Good:** + +```yaml +jobs: + build: + name: Build module + steps: + - name: Checkout repository + uses: actions/checkout@ # vX.Y.Z +``` + +**Bad:** + +```yaml +jobs: + build: + steps: + - uses: actions/checkout@ # vX.Y.Z +``` + +## Quoting + +- Only quote scalar values when YAML would otherwise misinterpret them +- Quote values starting with `{`, containing `:`, boolean-like strings (`true`/`false`), or numeric strings +- Omit quotes everywhere else + +**Good:** + +```yaml +run-name: Release ${{ github.ref_name }} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +``` + +**Bad:** + +```yaml +run-name: 'Release ${{ github.ref_name }}' +concurrency: + group: '${{ github.workflow }}-${{ github.ref }}' +``` + +## Pinning actions + +- Pin every `uses:` to a full 40-character commit SHA +- Add a patch-level version comment (`# vX.Y.Z`) so Dependabot can update SHA and comment together +- Applies to reusable workflows too (`uses: org/repo/.github/workflows/foo.yml@`) +- Exception: first-party actions in the same repository (`uses: ./.github/actions/...`) +- Resolve SHAs via `gh api` — never invent or guess a commit SHA. Always use the commit SHA, never the annotated-tag object SHA. + +**Good:** + +```yaml +- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 +``` + +**Bad:** + +```yaml +- uses: actions/checkout@v4 +- uses: actions/checkout@main +``` + +Resolving SHAs: + +```bash +gh api repos///git/refs/tags/ --jq '.object.sha' +# If .object.type == "tag", dereference: +gh api repos///git/tags/ --jq '.object.sha' +``` + +## Permissions + +- Declare workflow-level `permissions:` set to the strictest needed (default `contents: read`) +- Override per-job when one job needs more +- Never use `permissions: write-all` or omit `permissions:` entirely +- Add an inline comment justifying any non-`read` grant + +**Good:** + +```yaml +permissions: + contents: read + +jobs: + deploy: + permissions: + contents: read + id-token: write # OIDC federation to AWS +``` + +## Secrets and configuration + +- Use `vars.*` for configuration: region, account ID, role ARN, environment name +- Use `secrets.*` for credentials: API tokens, passwords, signing keys +- Never hardcode account IDs, role ARNs, or region names +- Authenticate to cloud providers with OIDC, never long-lived keys +- In reusable workflows, pass secrets explicitly — never use `secrets: inherit` + +**Good:** + +```yaml +permissions: + id-token: write + contents: read + +steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@ # vX.Y.Z + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: ${{ vars.AWS_ROLE_ARN_CONTINUOUS_DEPLOYMENT }} +``` + +**Bad:** + +```yaml +jobs: + call: + uses: ./.github/workflows/reusable.yml + secrets: inherit +``` + +## Untrusted input + +- Never interpolate untrusted context directly into shell commands +- Untrusted contexts include `github.event.issue.title`, `.body`, `.comment.body`, `.pull_request.title`, `.pull_request.body`, `.pull_request.head.ref`, `.head_commit.message`, `.review.body`, `.review_comment.body`, and `.head_ref` +- Pass untrusted values through an `env:` variable and quote them in the shell + +**Good:** + +```yaml +- run: echo "Title: $TITLE" + env: + TITLE: ${{ github.event.issue.title }} +``` + +**Bad:** + +```yaml +- run: echo "Title: ${{ github.event.issue.title }}" +``` + +## Triggers and isolation + +- Default to `pull_request` for PR validation +- Avoid `pull_request_target` and `workflow_run` unless required — they run with write tokens and secrets while potentially checking out attacker-controlled code +- If `pull_request_target` is unavoidable, never check out the PR head, or check out into a sandbox without secrets +- Use `actions/checkout` with `persist-credentials: false` unless the job pushes commits + +**Good:** + +```yaml +- uses: actions/checkout@ # vX.Y.Z + with: + persist-credentials: false +``` + +## Runners and concurrency + +- Pin `runs-on:` to a specific OS version (`ubuntu-24.04`) over a floating label (`ubuntu-latest`) +- Add `concurrency:` to deploy and release workflows +- Use `cancel-in-progress: true` for CI and `false` for deploys + +**Good:** + +```yaml +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false +``` + +## Operating protocol + +Before writing or reviewing a workflow: + +1. Read existing workflows and match shell choice, naming, job structure, and comment style +1. Check `.github/dependabot.yml` for `package-ecosystem: github-actions` and propose adding it if absent +1. Check `CODEOWNERS` for `.github/workflows/` ownership and recommend it if absent +1. Resolve SHAs via `gh api` — never invent a commit SHA + +## Security checklist + +Run mentally (and via [zizmor](https://github.com/woodruffw/zizmor) when available) before declaring done. + +### High severity — must fix + +- `template-injection` — no `${{ ... }}` from untrusted context inside `run:` or `script:` +- `dangerous-triggers` — `pull_request_target` / `workflow_run` justified and hardened +- `unpinned-uses` — every `uses:` has a 40-char SHA and `# vX.Y.Z` comment +- `excessive-permissions` — workflow- and job-level `permissions:` minimal +- `secrets-inherit` — no `secrets: inherit` +- `known-vulnerable-actions` — no pinned versions in GHSA +- `github-env` — no untrusted writes to `$GITHUB_ENV` / `$GITHUB_PATH` + +### Medium severity — fix unless justified + +- `overprovisioned-secrets` — no `${{ toJSON(secrets) }}` or wholesale `${{ secrets }}` +- `cache-poisoning` — no `actions/cache` (or `setup-*` cache) in tag-triggered release workflows +- `ref-confusion` — pinned ref is not a name shared by both a tag and a branch +- `ref-version-mismatch` — the `# vX.Y.Z` comment matches what the SHA actually is + +### Low severity — fix when reasonable + +- `stale-action-refs` — pinned commit corresponds to a real tag +- `impostor-commit` — pinned SHA exists in the action repo's history + +## Related resources + +- [GitHub Actions documentation](https://docs.github.com/en/actions) +- [zizmor — static analysis for GitHub Actions](https://github.com/woodruffw/zizmor) +- [GitHub Actions Standard](../GitHub-Actions/Standards.md) diff --git a/src/docs/Style-Guides/Markdown.md b/src/docs/Style-Guides/Markdown.md new file mode 100644 index 0000000..c8f55f4 --- /dev/null +++ b/src/docs/Style-Guides/Markdown.md @@ -0,0 +1,304 @@ +--- +title: Markdown +description: Markdown style guidelines for consistency across documentation. +--- + +# Markdown style guidelines + +This document defines the Markdown style guidelines for all Markdown files in this repository. These rules follow common Markdown linter best practices and ensure consistency across documentation. + +## Headings + +- Use ATX-style headings (`#`) instead of Setext-style (underlines) +- Include a space after the hash marks: `# Heading` not `#Heading` +- Use only one top-level heading (`#`) per document +- Do not skip heading levels (e.g., don't go from `#` to `###`) +- Surround headings with blank lines (one before, one after, excluding the first heading in the document unless preceded by frontmatter) +- Do not use trailing punctuation in headings (no periods, colons, etc.) +- Use sentence case for headings unless referring to proper nouns or code identifiers + +**Good:** + +```markdown +# Main heading + +## Subsection + +### Details +``` + +**Bad:** + +```markdown +#No space after hash +### Skipped level 2 +## Heading with period. +``` + +## Lists + +- Use consistent list markers throughout the document (`-` for unordered, `1.` for ordered) +- Do not add blank lines between list items (unless item contains multiple paragraphs) +- Indent nested lists by 2 spaces for unordered, 3 spaces for ordered +- Use `1.` for all ordered list items (auto-numbering) or number them sequentially +- Surround lists with blank lines (one before, one after) +- Use `-` for unordered lists (not `*` or `+`) + +**Good:** + +```markdown +Here is a list: + +- First item +- Second item +- Third item + +Another list: + +1. First step +1. Second step +1. Third step +``` + +**Bad:** + +```markdown +No blank line before list: +- Item one + +- Blank lines between items +- Not needed + +* Wrong marker ++ Mixed markers +``` + +## Code Blocks + +- Always use fenced code blocks (triple backticks) with language identifiers +- Always include a blank line before and after code blocks +- Specify the language for syntax highlighting (`bash`, `python`, `markdown`, `json`, etc.) +- Use `plaintext` or `text` if no specific language applies +- Indent code blocks at the same level as surrounding content + +**Good:** + +```markdown +Here is an example: + +\`\`\`bash +echo "Hello, world!" +\`\`\` + +The command prints a message. +``` + +**Bad:** + +```markdown +No language identifier: +\`\`\` +code here +\`\`\` +No blank lines before/after code blocks. +``` + +## Links + +- Use reference-style links for repeated URLs +- Use relative paths for internal links (relative to the current file) +- Always provide link text in square brackets: `[text](url)` +- Do not use bare URLs (wrap them: ``) +- For internal repository links, use relative paths starting with `./` or `../` +- Use `.md` extension for links to Markdown files + +**Good:** + +```markdown +See the [installation guide](../docs/installation.md) for details. + +Check out [GitHub][gh] and [GitLab][gl] for hosting. + +[gh]: https://github.com +[gl]: https://gitlab.com +``` + +**Bad:** + +```markdown +Absolute path: [guide](/docs/installation.md) +Missing extension: [guide](../docs/installation) +Bare URL: Visit https://example.com +``` + +## Tables + +- Use tables when content follows a consistent structure (instead of lists) +- Align columns using hyphens for readability +- Include header row separator with at least 3 hyphens per column +- Surround tables with blank lines (one before, one after) +- Use pipes (`|`) to separate columns +- Align content within columns for readability (optional but recommended) + +**Good:** + +```markdown +Here is a comparison: + +| Feature | Supported | Notes | +|---------|-----------|-------| +| Feature A | Yes | Fully supported | +| Feature B | No | Planned for v2 | +| Feature C | Partial | Beta feature | + +The table shows current status. +``` + +**Bad:** + +```markdown +Using list when table is better: +- Feature A: Yes - Fully supported +- Feature B: No - Planned for v2 +- Feature C: Partial - Beta feature +``` + +## Requirement Number Formatting + +When writing or referencing requirement numbers (NFR and FR) in documentation: +- **Always use bold formatting** for requirement numbers +- **Replace hyphens with non-breaking hyphens** (`‑`) between letters and numbers +- This prevents line breaks within requirement numbers and ensures consistent formatting +- This applies to all specification documents, plans, and tables + +Examples: +```markdown +# Correct formatting +**NFR‑001**: The system must respond within 200ms +**FR‑042**: User authentication shall support OAuth 2.0 + +# In tables +| ID | Description | +|----|-------------| +| **NFR‑001** | Performance requirement | +| **FR‑042** | Authentication feature | + +# Incorrect formatting (do not use) +NFR-001: Without bold or non-breaking hyphen +**NFR-001**: Bold but with regular hyphen (can break across lines) +NFR‑001: Non-breaking hyphen but not bold +``` + +## Emphasis + +- Use `*` or `_` for emphasis (italic), `**` or `__` for strong emphasis (bold) +- Be consistent within a document (prefer `*` and `**`) +- Do not use emphasis for headings +- Use backticks for code/technical terms, not emphasis + +**Good:** + +```markdown +This is *emphasized* text. +This is **strong** text. +Use the `--verbose` flag for details. +``` + +**Bad:** + +```markdown +This is _emphasized_ text with **strong** mixed styles. +Use the *--verbose* flag (should be backticks). +``` + +## Line Length + +- Wrap prose at 80-120 characters per line +- Do not wrap code blocks, tables, or URLs +- Break after sentences or at natural phrase boundaries +- Empty lines do not count toward line length + +## Whitespace + +- Use a single blank line to separate blocks of content +- Do not use multiple consecutive blank lines +- End files with a single newline character +- Do not use trailing whitespace at the end of lines +- Use spaces (not tabs) for indentation + +## Other Rules + +### Horizontal Rules + +- Use three hyphens (`---`) for horizontal rules +- Surround horizontal rules with blank lines + +**Good:** + +```markdown +Section one content. + +--- + +Section two content. +``` + +### Blockquotes + +- Use `>` for blockquotes with a space after +- Surround blockquotes with blank lines +- Use multiple `>` for nested quotes + +**Good:** + +```markdown +As the docs state: + +> This is an important note. +> It spans multiple lines. + +Back to regular text. +``` + +### Images + +- Use alt text for all images: `![alt text](path/to/image.png)` +- Use relative paths for repository images +- Prefer reference-style for repeated images + +**Good:** + +```markdown +![Architecture diagram](../media/architecture.png) + +See the [logo][logo-img] above. + +[logo-img]: ./images/logo.png +``` + +### HTML + +- Avoid HTML in Markdown when possible +- Use HTML only for features not supported by Markdown +- Close all HTML tags properly + +### Filenames + +- Use lowercase for Markdown filenames +- Use hyphens (`-`) not underscores (`_`) to separate words +- Use `.md` extension (not `.markdown`) + +**Examples:** + +- `installation-guide.md` ✅ +- `Installation_Guide.markdown` ❌ + +## Linting + +To validate Markdown files against these guidelines, use a Markdown linter such as: + +- [markdownlint](https://github.com/DavidAnson/markdownlint) +- [remark-lint](https://github.com/remarkjs/remark-lint) +- [superlinter](https://github.com/super-linter/super-linter) + +Configure the linter to enforce these rules in your CI/CD pipeline. diff --git a/src/docs/Style-Guides/PowerShell.md b/src/docs/Style-Guides/PowerShell.md new file mode 100644 index 0000000..8852edc --- /dev/null +++ b/src/docs/Style-Guides/PowerShell.md @@ -0,0 +1,867 @@ +--- +title: PowerShell +description: PowerShell style guidelines for consistency across scripts and modules. +--- + +# PowerShell style guidelines + +This document defines the PowerShell style guidelines for all PowerShell files in this repository. These rules follow PowerShell best practices, the One True Brace Style (OTBS), and community standards. + +## Brace Style (OTBS - One True Brace Style) + +- Opening braces on the same line as the statement (OTBS) +- Closing braces on their own line, aligned with the statement +- Use braces even for single-line statements in control structures +- No empty lines immediately after opening braces or before closing braces + +**Good:** + +```powershell +function Get-Example { + param($Name) + + if ($Name) { + Write-Output "Hello, $Name" + } else { + Write-Output "Hello, World" + } +} + +foreach ($item in $items) { + Get-Item $item +} +``` + +**Bad:** + +```powershell +function Get-Example +{ + # Opening brace should be on same line +} + +if ($condition) + Write-Output "Missing braces" + +if ($condition) { Write-Output "All on one line" } +``` + +## Naming Conventions + +### Functions and Cmdlets + +- Use approved PowerShell verbs (Get, Set, New, Remove, etc.) +- Follow Verb-Noun naming pattern with PascalCase +- Use singular nouns +- Be specific and descriptive + +**Good:** + +```powershell +function Get-UserProfile { } +function Set-ConfigValue { } +function New-DatabaseConnection { } +function Remove-TempFile { } +``` + +**Bad:** + +```powershell +function GetUser { } # Missing hyphen +function get-user { } # Wrong case +function Do-Something { } # Non-standard verb +function Get-Users { } # Should be singular unless always plural +``` + +### Parameters and Variables + +- Use PascalCase for parameters and public variables +- Use camelCase for private/local variables +- Use descriptive names, avoid abbreviations unless well-known +- Prefix boolean variables with verbs like `is`, `has`, `should` +- Append 'At' 'In' 'On' for timestamp/location variables +- Use `$_` for pipeline variables +- Avoid using reserved words as names +- Avoid using automatic variables for custom variables +- Parameter and variable names can be alphanumeric and include underscores. +- The colon character `:` and `.` are significant in PowerShell syntax, if they are a part of text, they must be escaped (`). + +**Good:** + +```powershell +$userName = "John" +$isValid = $true +$hasPermission = $false +$totalCount = 0 + +param( + [string]$ConfigPath, + [switch]$Force +) +``` + +**Bad:** + +```powershell +$usr = "John" # Too abbreviated +$valid = $true # Boolean should be $isValid +$TOTAL_COUNT = 0 # Wrong case style +``` + +### Constants + +- Use PascalCase with descriptive names +- Mark as `[System.Management.Automation.Language.ReadOnlyAttribute]` or use `Set-Variable -Option ReadOnly` + +**Good:** + +```powershell +$MaxRetries = 3 +$DefaultTimeout = 30 +Set-Variable -Name ApiEndpoint -Value "https://api.example.com" -Option ReadOnly +``` + +## Parameters + +- Always use `[OutputType()]`, `[CmdletBinding()]` and `param()` block at the top of functions +- Use parameter attributes for validation +- Provide meaningful parameter names with PascalCase +- Use type constraints for parameters. +- Have a space between the type and the parameter name +- Group mandatory parameters first +- Use `[switch]` for boolean flags that default to `$false`. +- Add help text with parameter descriptions (inside the param block) + + +**Good:** + +```powershell +function Get-UserData { + <# + .SYNOPSIS + Retrieves user data from the database. + + .DESCRIPTION + Retrieves user data from the database. + + .EXAMPLE + Get-UserData -UserId "12345" -IncludeDeleted + #> + [CmdletBinding()] + param( + # The unique identifier of the user. + [Parameter(Mandatory, Position = 0)] + [ValidateNotNullOrEmpty()] + [string] $UserId, + + # Include deleted users in the results. + [Parameter()] + [switch] $IncludeDeleted + ) + + # Function body +} +``` + +**Bad:** + +```powershell +function Get-UserData($id, $del) { + # No param block, no types, unclear names +} +``` + +## Indentation and Whitespace + +- Use 4 spaces for indentation (not tabs) +- No trailing whitespace at end of lines +- End files with a single newline character +- Use blank lines to separate logical blocks of code +- No blank lines immediately after opening braces or before closing braces +- One space after commas in arrays and parameters +- One space around operators (`=`, `+`, `-`, `-eq`, `-ne`, etc.) + +**Good:** + +```powershell +function Process-Data { + <# + ... Docs + #> + [CmdletBinding()] + param( + [Parameter(Mandatory, Position = 0)] + [ValidateNotNullOrEmpty()] + [array] $Items + ) + + $results = @() + + foreach ($item in $Items) { + $processed = Format-Item $item + $results += $processed + } + + return $results +} +``` + +**Bad:** + +```powershell +function Process-Data{ + param($Items) + + $results=@() + foreach($item in $Items){ + $processed=Format-Item $item + $results+=$processed + } + + return $results + +} +``` + +## Comments + +- Use `#` for single-line comments +- Use `<# ... #>` for multi-line comments and help documentation +- Place comments on their own line above the code they describe +- Use comment-based help for functions (`.SYNOPSIS`, `.DESCRIPTION`, `.EXAMPLE`) +- Put comment-based help first, inside the function body, before any code. This makes it cleaner to move, and collapse code. +- Do not use '.PARAMETER' for parameters in comment-based help, use inline comments instead inside the param block above + each of the parameters. +- Each section of comment-based help should have a blank line between them. +- The comment-based help must be indented to align with the function definition. +- Keep comments up to date with code changes. +- The code should say what it is doing, comments should explain why. + +**Good:** + +```powershell +function New-UserAccount { +<# + .SYNOPSIS + Creates a new user account. + + .DESCRIPTION + Creates a new user account with the specified username and email. + Validates the email format before creation. + + .EXAMPLE + New-UserAccount -UserName 'jdoe' -Email 'jdoe@example.com' + + Creates a new user account for jdoe with email jdoe@example.com. + + .LINK + https://example.com/docs/New-UserAccount +#> + [CmdletBinding()] + param( + # The username for the new account. + [Parameter(Mandatory)] + [string] $UserName, + + # The email address for the new account. + [Parameter(Mandatory)] + [string] $Email + ) + + # Validate email format before processing + if ($Email -notmatch '^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') { + throw "Invalid email format" + } + + # Create the user account + New-Object PSObject -Property @{ + UserName = $UserName + Email = $Email + } +} +``` + +**Bad:** + +```powershell +function New-UserAccount { + param($UserName, $Email) + # Check email + if ($Email -notmatch '^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') { throw "Invalid email format" } + New-Object PSObject -Property @{UserName = $UserName; Email = $Email} # Create user +} +``` + +## String Handling + +- Use single quotes for strings that don't need variable expansion +- Use double quotes only for strings with variables or escape sequences +- Use here-strings (`@"..."@` or `@'...'@`) for multi-line strings +- Use `-f` operator or string interpolation for formatting +- Avoid string concatenation with `+` in loops (use arrays or StringBuilder) + +**Good:** + +```powershell +$name = 'John' +$greeting = "Hello, $name" +$path = 'C:\Temp\file.txt' +$message = "The value is: {0}" -f $value + +$multiLine = @" +This is a +multi-line string +with variable: $name +"@ +``` + +**Bad:** + +```powershell +$name = "John" # Should use single quotes (no variables) +$greeting = 'Hello, $name' # Variable won't expand +$message = "The value is: " + $value # Use formatting instead +``` + +## Control Structures + +### If/Else Statements + +- Always use braces, even for single statements +- Opening brace on same line (OTBS) +- Use `elseif` (one word) not `else if` +- One space before opening brace +- Comparison operators on separate lines for readability in complex conditions + +**Good:** + +```powershell +if ($condition) { + Do-Something +} elseif ($otherCondition) { + Do-SomethingElse +} else { + Do-Default +} + +# Complex condition +if ($user.IsActive -and + $user.HasPermission -and + $user.Age -gt 18) { + Grant-Access +} +``` + +**Bad:** + +```powershell +if ($condition) +{ # Brace should be on same line + Do-Something +} + +if ($condition) Do-Something # Missing braces + +if($condition){ # Missing spaces + Do-Something +} +``` + +### Loops + +- Use appropriate loop construct (foreach, for, while, do-while) +- Always use braces +- Opening brace on same line (OTBS) +- Prefer `foreach` for collections over `for` when index not needed + +**Good:** + +```powershell +foreach ($item in $collection) { + Process-Item $item +} + +for ($i = 0; $i -lt $count; $i++) { + Process-Index $i +} + +while ($condition) { + Update-Condition +} +``` + +**Bad:** + +```powershell +foreach ($item in $collection) +{ # Brace should be on same line + Process-Item $item +} + +foreach ($item in $collection) Process-Item $item # Missing braces +``` + +### Switch Statements + +- Opening brace on same line +- Indent case statements by 4 spaces +- Use `break` or `continue` explicitly when needed +- Use `default` for fallback cases + +**Good:** + +```powershell +switch ($value) { + 'Option1' { + Do-FirstThing + } + 'Option2' { + Do-SecondThing + } + default { + Do-DefaultThing + } +} +``` + +## Error Handling + +- Use try/catch/finally blocks for error handling +- Be specific with catch blocks (catch specific exception types) +- Always provide meaningful error messages +- Use `throw` for unrecoverable errors +- Use `Write-Error` for non-terminating errors +- Set `$ErrorActionPreference` appropriately + +**Good:** + +```powershell +function Get-FileContent { + param([string]$Path) + + try { + if (-not (Test-Path $Path)) { + throw "File not found: $Path" + } + + $content = Get-Content -Path $Path -ErrorAction Stop + return $content + } catch [System.IO.IOException] { + Write-Error "IO error reading file: $_" + throw + } catch { + Write-Error "Unexpected error: $_" + throw + } finally { + # Cleanup code here + } +} +``` + +**Bad:** + +```powershell +function Get-FileContent { + param([string]$Path) + + try { + Get-Content -Path $Path + } catch { + # Swallowing errors silently + } +} +``` + +## Output and Logging + +- Use `Write-Output` for function return values (or implicit return) +- Avoid `Write-Host` use `Write-Information` or `Write-Output` instead. +- Use `Write-Verbose` for detailed operation information +- Use `Write-Debug` for debugging information +- Use `Write-Warning` for warnings +- Use `Write-Error` for errors +- Use `Write-Information` for informational messages (PS 5.0+) + +**Good:** + +```powershell +function Get-ProcessedData { + [CmdletBinding()] + param($Data) + + Write-Verbose "Processing $($Data.Count) items" + + $result = Process-Data $Data + + if ($result.Warnings) { + Write-Warning "Processing completed with warnings" + } + + Write-Output $result +} +``` + +**Bad:** + +```powershell +function Get-ProcessedData { + param($Data) + + Write-Host "Processing data..." # Should use Write-Verbose + + $result = Process-Data $Data + + Write-Host $result # Should use Write-Output +} +``` + +## Suppressing Output + +- Use `$null =` to suppress unwanted output from commands +- Avoid using `| Out-Null` as it is significantly slower +- Use `[void]` for method calls that return values you want to discard +- This is especially important in loops or performance-critical code + +**Good:** + +```powershell +# Suppress output from .NET method calls +$null = $list.Add($item) +$null = $collection.Remove($item) + +# Alternative for methods +[void]$list.Add($item) + +# Suppress output from cmdlets +$null = New-Item -Path $path -ItemType Directory +``` + +**Bad:** + +```powershell +# Slower performance with Out-Null +$list.Add($item) | Out-Null +$collection.Remove($item) | Out-Null +New-Item -Path $path -ItemType Directory | Out-Null +``` + +## Pipeline + +- Design functions to accept pipeline input when appropriate +- Use `[Parameter(ValueFromPipeline = $true)]` for pipeline parameters +- Implement `process` block for pipeline-aware functions +- Use `begin` and `end` blocks when initialization or cleanup needed + +**Good:** + +```powershell +function Update-Item { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [PSCustomObject]$Item + ) + + begin { + $count = 0 + } + + process { + # Process each item from pipeline + $Item.LastModified = Get-Date + $count++ + Write-Output $Item + } + + end { + Write-Verbose "Processed $count items" + } +} + +# Usage +$items | Update-Item +``` + +## Arrays and Hashtables + +- Use `@()` for empty arrays +- Use `@{}` for empty hashtables +- Use consistent formatting for multi-line collections +- One item per line for readability in multi-line collections +- Trailing comma optional but allowed on last item +- Align key-value pairs in hashtables for readability +- Use splatting for functions with many parameters + +**Good:** + +```powershell +$emptyArray = @() +$numbers = @(1, 2, 3, 4, 5) + +$multiLineArray = @( + 'First item' + 'Second item' + 'Third item' +) + +$hashtable = @{ + Name = 'John' + Age = 30 + City = 'Seattle' +} + +$complexHash = @{ + Server = @{ + Name = 'WebServer01' + Port = 8080 + } + Database = @{ + Name = 'MainDB' + Port = 5432 + } +} +``` + +**Bad:** + +```powershell +$array = 1, 2, 3, 4, 5 # Use @() syntax + +$hashtable = @{Name = 'John'; Age = 30; City = 'Seattle'} # Multi-line for readability +``` + +## Splatting + +- Use splatting for functions with many parameters +- Create hashtable with parameters before splatting +- Use `@` symbol for splatting (not `$`) + +**Good:** + +```powershell +$params = @{ + Path = 'C:\Temp' + Filter = '*.txt' + Recurse = $true + ErrorAction = 'Stop' +} + +Get-ChildItem @params +``` + +**Bad:** + +```powershell +Get-ChildItem -Path 'C:\Temp' -Filter '*.txt' -Recurse $true -ErrorAction 'Stop' +``` + +## Comparison Operators + +- Use PowerShell comparison operators (`-eq`, `-ne`, `-gt`, `-lt`, `-ge`, `-le`) +- Don't use C-style operators (`==`, `!=`, `>`, `<`) +- Use `-like` for wildcard matching, `-match` for regular expression +- Use `-contains` for collection membership, not `-eq` +- Add `-i` prefix for case-insensitive (default) or `-c` for case-sensitive +- Caution with $null comparisons. Comparison order is important depending if the variable is a single item or a collection. + - `$null -eq $var` is usually safer for collections as it won't error if $var is $null or empty. + +**Good:** + +```powershell +if ($value -eq 10) { } +if ($name -like 'John*') { } +if ($email -match '^[\w-\.]+@') { } +if ($list -contains $item) { } +if ($name -ceq 'JOHN') { } # Case-sensitive +if ($null -eq $collection) { } # Safe null check for collections +``` + +**Bad:** + +```powershell +if ($value == 10) { } # Wrong operator +if ($list -eq $item) { } # Use -contains for collections +if ($collection -eq $null) { } # Can error if $collection is $null or empty +``` + +## Script Structure + +- Use `#Requires` statements at the top for version/module requirements +- Place param block after `#Requires` and comment-based help +- Group related functions together +- Separate sections with comments +- End script with single newline + +**Good:** + +```powershell +#Requires -Version 7.4 + +<# + .SYNOPSIS + Script for managing user accounts. + + .DESCRIPTION + This script provides functions to create, update, and delete user accounts. +#> + +[CmdletBinding()] +param( + [string]$ConfigPath = ".\config.json" +) + +# Script-level variables +$ErrorActionPreference = 'Stop' + +#region Helper functions +function Get-ConfigData { + param([string]$Path) + # Implementation +} + +function New-UserAccount { + param($UserName, $Email) + # Implementation +} +#endregion + +# Script execution +try { + $config = Get-ConfigData -Path $ConfigPath + # Main logic +} catch { + Write-Error "Script failed: $_" + exit 1 +} +``` + +## Performance Considerations + +- Avoid `Write-Host` in production scripts, instead use `Write-Information` +- Use `ArrayList` collection instead of `@()` arrays in loops. +- Use `-Filter` parameter instead of piping to `Where-Object` when available +- Avoid unnecessary pipeline operations +- Use `.ForEach()` and `.Where()` methods for better performance on large collections + +**Good:** + +```powershell +# Efficient array building +$results = [System.Collections.Generic.List[PSObject]]::new() +foreach ($item in $collection) { + $results.Add($processedItem) +} + +# Efficient filtering +Get-ChildItem -Path C:\Temp -Filter *.txt + +# Method syntax for performance +$filtered = $collection.Where({ $_.Value -gt 10 }) +``` + +**Bad:** + +```powershell +# Inefficient array building +$results = @() +foreach ($item in $collection) { + $results += $processedItem # Creates new array each iteration +} + +# Inefficient filtering +Get-ChildItem -Path C:\Temp | Where-Object { $_.Name -like '*.txt' } +``` + +## Line Length + +- Wrap lines at 100-120 characters +- Use backtick (`` ` ``) for line continuation (sparingly), prefer splatting +- Prefer breaking at natural points (after commas, operators, pipes) +- Align continued lines for readability + +**Good:** + +```powershell +$result = Get-Something -Parameter1 $value1 ` + -Parameter2 $value2 ` + -Parameter3 $value3 + +# Better: Use splatting +$params = @{ + Parameter1 = $value1 + Parameter2 = $value2 + Parameter3 = $value3 +} +$result = Get-Something @params +``` + +## Testing + +- Write Pester tests for all functions +- Name test files `*.Tests.ps1` +- Group tests with `Describe` and `Context` blocks +- Use `It` blocks for individual test cases +- Use `Should` assertions +- Use `BeforeAll` and `AfterAll` for setup/teardown + +**Good:** + +```powershell +Describe 'Get-UserAccount' { + Context 'When user exists' { + It 'Should return user object' { + $result = Get-UserAccount -UserId '123' + $result | Should -Not -BeNullOrEmpty + $result.UserId | Should -Be '123' + } + } + + Context 'When user does not exist' { + It 'Should throw error' { + { Get-UserAccount -UserId '999' } | Should -Throw + } + } +} +``` + +## Security Best Practices + +- Never hardcode credentials or secrets +- Use `SecureString` for sensitive data +- Use `Get-Credential` for credential prompts +- Validate all user input +- Use `-WhatIf` and `-Confirm` for destructive operations +- Avoid `Invoke-Expression` with user input + +**Good:** + +```powershell +function Remove-UserData { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$UserId + ) + + if ($PSCmdlet.ShouldProcess($UserId, "Remove user data")) { + # Perform deletion + } +} + +# Get credentials securely +$cred = Get-Credential -Message "Enter admin credentials" +``` + +**Bad:** + +```powershell +$password = "MyPassword123" # Hardcoded password +Invoke-Expression $userInput # Security risk +``` + +## Related Resources + +- [PowerShell Practice and Style Guide](https://poshcode.gitbook.io/powershell-practice-and-style/) +- [PowerShell Best Practices](https://docs.microsoft.com/en-us/powershell/scripting/developer/cmdlet/cmdlet-development-guidelines) +- [PSScriptAnalyzer Rules](https://github.com/PowerShell/PSScriptAnalyzer) diff --git a/src/docs/Style-Guides/index.md b/src/docs/Style-Guides/index.md new file mode 100644 index 0000000..4952538 --- /dev/null +++ b/src/docs/Style-Guides/index.md @@ -0,0 +1,16 @@ +--- +title: Style Guides +description: Coding style guidelines for PSModule repositories. +--- + +# Style guides + +These style guides define the coding conventions that keep PSModule repositories consistent, readable, and easy to maintain. They are the single source of truth for how we write code and documentation, and they back the editor instructions and review automation used across the organization. + +Each guide states the rules, shows good and bad examples, and explains how to apply them. + +| Guide | What it covers | +| ----- | -------------- | +| [GitHub Actions](GitHub-Actions.md) | Workflow and composite action authoring, naming, and security | +| [Markdown](Markdown.md) | Headings, lists, code blocks, links, tables, and formatting | +| [PowerShell](PowerShell.md) | Brace style, naming, parameters, error handling, and testing | diff --git a/src/zensical.toml b/src/zensical.toml index 4b8985a..946c0e5 100644 --- a/src/zensical.toml +++ b/src/zensical.toml @@ -52,6 +52,12 @@ nav = [ "GitHub-Actions/index.md", {"Standards" = "GitHub-Actions/Standards.md"}, ]}, + {"Style Guides" = [ + "Style-Guides/index.md", + {"GitHub Actions" = "Style-Guides/GitHub-Actions.md"}, + {"Markdown" = "Style-Guides/Markdown.md"}, + {"PowerShell" = "Style-Guides/PowerShell.md"}, + ]}, {"Solutions" = [ "Solutions/index.md", {"GitHub Copilot Customization" = "Solutions/GitHub-Copilot-Customization-Architecture.md"},