diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 904dd4a..10c7f7a 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -4,6 +4,9 @@ on: release: types: [published] +permissions: + contents: read + jobs: publish: name: Publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d4dd83b..9829f76 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,25 +1,19 @@ name: Test +permissions: + contents: read + checks: write + pull-requests: write + issues: write on: push: - branches: [ $default-branch ] + branches: [ main ] pull_request: workflow_dispatch: jobs: - test: - name: Test - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] - steps: - - uses: actions/checkout@v4 - - name: Test - shell: pwsh - env: - DEBUG: ${{ runner.debug == '1' }} - run: | - if($env:DEBUG -eq 'true' -or $env:DEBUG -eq '1') { - $DebugPreference = 'Continue' - } - ./build.ps1 -Task Test -Bootstrap + # Delegate to the psake org's shared module CI workflow. It runs the full build/test suite + # (Build + Analyze + Pester) on PowerShell 7+ across Linux/Windows/macOS and on the real + # Windows PowerShell 5.1 (Desktop) engine, so regressions like the 0.8.0 ternary that broke + # module import on 5.1 are caught by the standard test run rather than a separate smoke test. + ci: + name: CI + uses: psake/.github/.github/workflows/ModuleCI.yml@main diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..177aaf5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,72 @@ +# AI Agent Instructions + +AI agents working in this repository must follow these instructions. + +Template Version: 0.8.14 + +Last sync: 2026-05-17 (Update this date when syncing from the centralized repository) + +## Instructions for AI Agents + +AI agents **must**: + +1. **When deploying or updating this template, follow `instructions/update.instructions.md` and + update the Last sync date above.** + +2. **Read `instructions/agent-workflow.instructions.md` FIRST to determine which other instruction + files apply to your task.** Follow all applicable instructions before proceeding with work. + +3. **Check `aim.config.json`** for module configuration and external source settings. + +## Instruction Applicability Matrix + +Use this matrix to determine which instruction files to read based on your task: + +| Task Type | Required Instructions | +| ---------------------------- | -------------------------------------- | +| Any task | `agent-workflow.instructions.md` | +| Any code or documentation | `shorthand.instructions.md` | +| Git operations | `git-workflow.instructions.md` | +| Writing tests | `testing.instructions.md` | +| PowerShell code | `powershell.instructions.md` | +| Documentation | `markdown.instructions.md` | +| README files | `readme.instructions.md` | +| GitHub CLI usage | `github-cli.instructions.md` | +| Creating releases | `releases.instructions.md` | +| Repository-specific work | `repository-specific.instructions.md` | +| Updating instructions | `update.instructions.md` | +| Contributing to upstream | `contributing.instructions.md` | + +## Available Instruction Files + +- `agent-workflow.instructions.md` - Pre-flight protocol and task workflow +- `shorthand.instructions.md` - Avoid shorthand and abbreviations +- `git-workflow.instructions.md` - Git branching, commits, and PR conventions +- `testing.instructions.md` - Test writing best practices +- `powershell.instructions.md` - PowerShell coding standards +- `markdown.instructions.md` - Markdown formatting standards +- `readme.instructions.md` - README maintenance guidelines +- `github-cli.instructions.md` - GitHub CLI usage guidelines +- `releases.instructions.md` - Release management guidelines +- `repository-specific.instructions.md` - Repository-specific customizations +- `update.instructions.md` - Procedures for updating instructions +- `contributing.instructions.md` - Contributing improvements to upstream + +## Quick Reference + +### Before Starting Any Task + +1. Identify the task type from the matrix above +2. Read all applicable instruction files +3. Follow the guidelines when implementing + +### Best Practices + +- Follow existing patterns in the codebase +- Keep solutions simple and focused +- Only make changes that are directly requested +- Follow language-specific guidelines + +## Repository-Specific Instructions + +See `instructions/repository-specific.instructions.md` for customizations specific to this repository. diff --git a/CHANGELOG.md b/CHANGELOG.md index e0dc162..b74bd8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +## [0.8.1] 2026-06-03 + +### Fixed + +- Restore Windows PowerShell 5.1 (Desktop edition) compatibility, which regressed + in 0.8.0. `Get-PSBuildCertificate` used the PowerShell 7+-only ternary operator, + causing the file to fail to parse and the whole module to fail to import under + Windows PowerShell 5.1 — even though the manifest still declares support for it. + The ternary is replaced with an `if`/`else` expression, and the `$IsWindows` + platform guard now treats the absent automatic variable on Desktop edition as + Windows (matching the existing pattern in `Build-PSBuildUpdatableHelp`). Behavior + on PowerShell 7+ is unchanged. + +## [0.8.0] 2026-02-20 + +### Added + +- [**#92**](https://github.com/psake/PowerShellBuild/pull/92) Add Authenticode + code-signing support for PowerShell modules with three new public functions: + - `Get-PSBuildCertificate` - Resolves code-signing X509Certificate2 objects + from certificate store, PFX files, Base64-encoded environment variables, + or pre-resolved certificate objects + - `Invoke-PSBuildModuleSigning` - Signs PowerShell module files (*.psd1, + *.psm1, *.ps1) with Authenticode signatures supporting configurable + timestamp servers and hash algorithms + - `New-PSBuildFileCatalog` - Creates Windows catalog (.cat) files for + tamper detection +- New build tasks for module signing pipeline: `SignModule`, `BuildCatalog`, + `SignCatalog`, `Sign` (meta-task) +- Extended `$PSBPreference.Sign` configuration section with certificate + source selection, timestamp server configuration, hash algorithm options, + and catalog generation settings + +### Fixed + - Remove extra backticks during localization text migration. ## [0.7.3] 2025-08-01 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/PowerShellBuild/IB.tasks.ps1 b/PowerShellBuild/IB.tasks.ps1 index e9d2d98..fe54942 100644 --- a/PowerShellBuild/IB.tasks.ps1 +++ b/PowerShellBuild/IB.tasks.ps1 @@ -3,26 +3,26 @@ Set-Variable -Name PSBPreference -Option ReadOnly -Scope Script -Value (. ([IO.P $__DefaultBuildDependencies = $PSBPreference.Build.Dependencies # Synopsis: Initialize build environment variables -task Init { +Task Init { Initialize-PSBuild -UseBuildHelpers -BuildEnvironment $PSBPreference } # Synopsis: Clears module output directory -task Clean Init, { +Task Clean Init, { Clear-PSBuildOutputFolder -Path $PSBPreference.Build.ModuleOutDir } # Synopsis: Builds module based on source directory -task StageFiles Clean, { +Task StageFiles Clean, { $buildParams = @{ - Path = $PSBPreference.General.SrcRootDir - ModuleName = $PSBPreference.General.ModuleName - DestinationPath = $PSBPreference.Build.ModuleOutDir - Exclude = $PSBPreference.Build.Exclude - Compile = $PSBPreference.Build.CompileModule - CompileDirectories = $PSBPreference.Build.CompileDirectories - CopyDirectories = $PSBPreference.Build.CopyDirectories - Culture = $PSBPreference.Help.DefaultLocale + Path = $PSBPreference.General.SrcRootDir + ModuleName = $PSBPreference.General.ModuleName + DestinationPath = $PSBPreference.Build.ModuleOutDir + Exclude = $PSBPreference.Build.Exclude + Compile = $PSBPreference.Build.CompileModule + CompileDirectories = $PSBPreference.Build.CompileDirectories + CopyDirectories = $PSBPreference.Build.CopyDirectories + Culture = $PSBPreference.Help.DefaultLocale } if ($PSBPreference.Help.ConvertReadMeToAboutHelp) { @@ -59,7 +59,7 @@ $analyzePreReqs = { } # Synopsis: Execute PSScriptAnalyzer tests -task Analyze -If (. $analyzePreReqs) Build,{ +Task Analyze -If (. $analyzePreReqs) Build, { $analyzeParams = @{ Path = $PSBPreference.Build.ModuleOutDir SeverityThreshold = $PSBPreference.Test.ScriptAnalysis.FailBuildOnSeverityLevel @@ -86,7 +86,7 @@ $pesterPreReqs = { } # Synopsis: Execute Pester tests -task Pester -If (. $pesterPreReqs) Build,{ +Task Pester -If (. $pesterPreReqs) Build, { $pesterParams = @{ Path = $PSBPreference.Test.RootDir ModuleName = $PSBPreference.General.ModuleName @@ -117,7 +117,7 @@ $genMarkdownPreReqs = { } # Synopsis: Generates PlatyPS markdown files from module help -task GenerateMarkdown -if (. $genMarkdownPreReqs) StageFiles,{ +Task GenerateMarkdown -if (. $genMarkdownPreReqs) StageFiles, { $buildMDParams = @{ ModulePath = $PSBPreference.Build.ModuleOutDir ModuleName = $PSBPreference.General.ModuleName @@ -141,7 +141,7 @@ $genHelpFilesPreReqs = { } # Synopsis: Generates MAML-based help from PlatyPS markdown files -task GenerateMAML -if (. $genHelpFilesPreReqs) GenerateMarkdown, { +Task GenerateMAML -if (. $genHelpFilesPreReqs) GenerateMarkdown, { Build-PSBuildMAMLHelp -Path $PSBPreference.Docs.RootDir -DestinationPath $PSBPreference.Build.ModuleOutDir } @@ -155,7 +155,7 @@ $genUpdatableHelpPreReqs = { } # Synopsis: Create updatable help .cab file based on PlatyPS markdown help -task GenerateUpdatableHelp -if (. $genUpdatableHelpPreReqs) BuildHelp, { +Task GenerateUpdatableHelp -if (. $genUpdatableHelpPreReqs) BuildHelp, { Build-PSBuildUpdatableHelp -DocsPath $PSBPreference.Docs.RootDir -OutputPath $PSBPreference.Help.UpdatableHelpOutDir } @@ -184,17 +184,149 @@ Task Publish Test, { #region Summary Tasks # Synopsis: Builds help documentation -task BuildHelp GenerateMarkdown,GenerateMAML +Task BuildHelp GenerateMarkdown, GenerateMAML Task Build { if ([String]$PSBPreference.Build.Dependencies -ne [String]$__DefaultBuildDependencies) { throw [NotSupportedException]'You cannot use $PSBPreference.Build.Dependencies with Invoke-Build. Please instead redefine the build task or your default task to include your dependencies. Example: Task . Dependency1,Dependency2,Build,Test or Task Build Dependency1,Dependency2,StageFiles' } -},StageFiles,BuildHelp +}, StageFiles, BuildHelp # Synopsis: Execute Pester and ScriptAnalyzer tests -task Test Analyze,Pester +Task Test Analyze, Pester -task . Build,Test +Task . Build, Test + +# Synopsis: Signs module files (*.psd1, *.psm1, *.ps1) with an Authenticode signature +Task SignModule -If { + if (-not $PSBPreference.Sign.Enabled) { + Write-Warning 'Module signing is not enabled.' + return $false + } + if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) { + Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.' + return $false + } + $true +} Build, { + $certParams = @{ + CertificateSource = $PSBPreference.Sign.CertificateSource + CertStoreLocation = $PSBPreference.Sign.CertStoreLocation + CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar + CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar + } + if ($PSBPreference.Sign.Thumbprint) { + $certParams.Thumbprint = $PSBPreference.Sign.Thumbprint + } + if ($PSBPreference.Sign.PfxFilePath) { + $certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath + } + if ($PSBPreference.Sign.PfxFilePassword) { + $certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword + } + + $certificate = if ($PSBPreference.Sign.Certificate) { + $PSBPreference.Sign.Certificate + } else { + Get-PSBuildCertificate @certParams + } + + if ($null -eq $certificate) { + throw $LocalizedData.NoCertificateFound + } + + $signingParams = @{ + Path = $PSBPreference.Build.ModuleOutDir + Certificate = $certificate + TimestampServer = $PSBPreference.Sign.TimestampServer + HashAlgorithm = $PSBPreference.Sign.HashAlgorithm + Include = $PSBPreference.Sign.FilesToSign + } + Invoke-PSBuildModuleSigning @signingParams +} + +# Synopsis: Creates a Windows catalog (.cat) file for the built module +Task BuildCatalog -If { + if (-not ($PSBPreference.Sign.Enabled -and $PSBPreference.Sign.Catalog.Enabled)) { + Write-Warning 'Catalog generation is not enabled.' + return $false + } + if (-not (Get-Command -Name 'New-FileCatalog' -ErrorAction Ignore)) { + Write-Warning 'New-FileCatalog is not available. Catalog generation requires Windows.' + return $false + } + $true +} SignModule, { + $catalogFileName = if ($PSBPreference.Sign.Catalog.FileName) { + $PSBPreference.Sign.Catalog.FileName + } else { + "$($PSBPreference.General.ModuleName).cat" + } + $catalogFilePath = Join-Path -Path $PSBPreference.Build.ModuleOutDir -ChildPath $catalogFileName + + $catalogParams = @{ + ModulePath = $PSBPreference.Build.ModuleOutDir + CatalogFilePath = $catalogFilePath + CatalogVersion = $PSBPreference.Sign.Catalog.Version + } + New-PSBuildFileCatalog @catalogParams +} + +# Synopsis: Signs the module catalog (.cat) file with an Authenticode signature +Task SignCatalog -If { + if (-not ($PSBPreference.Sign.Enabled -and $PSBPreference.Sign.Catalog.Enabled)) { + Write-Warning 'Catalog signing is not enabled.' + return $false + } + if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) { + Write-Warning 'Set-AuthenticodeSignature is not available. Catalog signing requires Windows.' + return $false + } + $true +} BuildCatalog, { + $certParams = @{ + CertificateSource = $PSBPreference.Sign.CertificateSource + CertStoreLocation = $PSBPreference.Sign.CertStoreLocation + CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar + CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar + } + if ($PSBPreference.Sign.Thumbprint) { + $certParams.Thumbprint = $PSBPreference.Sign.Thumbprint + } + if ($PSBPreference.Sign.PfxFilePath) { + $certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath + } + if ($PSBPreference.Sign.PfxFilePassword) { + $certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword + } + + $certificate = if ($PSBPreference.Sign.Certificate) { + $PSBPreference.Sign.Certificate + } else { + Get-PSBuildCertificate @certParams + } + + if ($null -eq $certificate) { + throw $LocalizedData.NoCertificateFound + } + + $catalogFileName = if ($PSBPreference.Sign.Catalog.FileName) { + $PSBPreference.Sign.Catalog.FileName + } else { + "$($PSBPreference.General.ModuleName).cat" + } + + $signingParams = @{ + Path = $PSBPreference.Build.ModuleOutDir + Certificate = $certificate + TimestampServer = $PSBPreference.Sign.TimestampServer + HashAlgorithm = $PSBPreference.Sign.HashAlgorithm + Include = @($catalogFileName) + } + Invoke-PSBuildModuleSigning @signingParams +} + +# Synopsis: Signs module files and catalog (meta task) +Task Sign SignModule, SignCatalog #endregion Summary Tasks diff --git a/PowerShellBuild/PowerShellBuild.psd1 b/PowerShellBuild/PowerShellBuild.psd1 index 39731b5..8b6a039 100644 --- a/PowerShellBuild/PowerShellBuild.psd1 +++ b/PowerShellBuild/PowerShellBuild.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PowerShellBuild.psm1' - ModuleVersion = '0.7.3' + ModuleVersion = '0.8.1' GUID = '15431eb8-be2d-4154-b8ad-4cb68a488e3d' Author = 'Brandon Olin' CompanyName = 'Community' @@ -19,7 +19,10 @@ 'Build-PSBuildModule' 'Build-PSBuildUpdatableHelp' 'Clear-PSBuildOutputFolder' + 'Get-PSBuildCertificate' 'Initialize-PSBuild' + 'Invoke-PSBuildModuleSigning' + 'New-PSBuildFileCatalog' 'Publish-PSBuildModule' 'Test-PSBuildPester' 'Test-PSBuildScriptAnalysis' diff --git a/PowerShellBuild/Public/Get-PSBuildCertificate.ps1 b/PowerShellBuild/Public/Get-PSBuildCertificate.ps1 new file mode 100644 index 0000000..c8bdbec --- /dev/null +++ b/PowerShellBuild/Public/Get-PSBuildCertificate.ps1 @@ -0,0 +1,204 @@ +function Get-PSBuildCertificate { + <# + .SYNOPSIS + Resolves a code-signing X509Certificate2 from one of several common sources. + .DESCRIPTION + Resolves a code-signing certificate suitable for use with Set-AuthenticodeSignature. + Supports five certificate sources to accommodate local development, CI/CD pipelines, + and custom signing infrastructure: + + Auto - Checks the CertificateEnvVar environment variable first. If it is + populated, uses EnvVar mode; otherwise falls back to Store mode. + This is the recommended default for projects that run both locally + and in automated pipelines. + + Store - Selects the first valid, unexpired code-signing certificate that has + a private key from the Windows certificate store at CertStoreLocation. + Suitable for developer workstations where a certificate is installed. + + Thumbprint - Like Store, but matches a specific certificate by its thumbprint. + Recommended when multiple code-signing certificates are installed and + you need a deterministic selection. + + EnvVar - Decodes a Base64-encoded PFX from an environment variable and + optionally decrypts it with a password from a second variable. + The most common approach for GitHub Actions, Azure DevOps Pipelines, + and GitLab CI where secrets are stored as masked variables. + + PfxFile - Loads a PFX/P12 file from disk with an optional SecureString password. + Useful for local scripts, containers, and environments where a + certificate file is mounted or distributed via a secrets manager. + + Note: Authenticode signing is a Windows-only capability. This function will fail + on non-Windows platforms when using Store or Thumbprint sources. + .PARAMETER CertificateSource + The source from which to resolve the code-signing certificate. + Valid values: Auto, Store, Thumbprint, EnvVar, PfxFile. Default: Auto. + .PARAMETER CertStoreLocation + Windows certificate store path to search when CertificateSource is Store or Thumbprint. + Default: Cert:\CurrentUser\My. + .PARAMETER Thumbprint + The exact certificate thumbprint to look up. Required when CertificateSource is Thumbprint. + .PARAMETER CertificateEnvVar + Name of the environment variable holding the Base64-encoded PFX certificate. + Used by the EnvVar source and by Auto as the presence-detection key. + Default: SIGNCERTIFICATE. + .PARAMETER CertificatePasswordEnvVar + Name of the environment variable holding the PFX password. Used by EnvVar source. + Default: CERTIFICATEPASSWORD. + .PARAMETER PfxFilePath + File system path to a PFX/P12 certificate file. Required when CertificateSource is PfxFile. + .PARAMETER PfxFilePassword + Password for the PFX file as a SecureString. Used by PfxFile source. + .PARAMETER SkipValidation + Skip validation checks (private key presence, expiration, Code Signing EKU) for certificates + loaded from EnvVar or PfxFile sources. Use with caution; invalid certificates will fail during + actual signing operations with less descriptive errors. + .OUTPUTS + System.Security.Cryptography.X509Certificates.X509Certificate2 + Returns the resolved certificate, or $null if none was found (Store/Thumbprint sources). + .EXAMPLE + PS> $cert = Get-PSBuildCertificate + + Resolve automatically: use the SIGNCERTIFICATE env var when present, otherwise search + the current user's certificate store. + .EXAMPLE + PS> $cert = Get-PSBuildCertificate -CertificateSource Store + + Explicitly load the first valid code-signing certificate from the current user's store. + .EXAMPLE + PS> $cert = Get-PSBuildCertificate -CertificateSource Thumbprint -Thumbprint 'AB12CD34EF56...' + + Load a specific certificate from the certificate store by its thumbprint. + .EXAMPLE + PS> $cert = Get-PSBuildCertificate -CertificateSource EnvVar ` + -CertificateEnvVar 'MY_PFX' -CertificatePasswordEnvVar 'MY_PFX_PASS' + + Decode a PFX certificate stored in a CI/CD secret environment variable. + .EXAMPLE + PS> $pass = Read-Host -Prompt 'Certificate password' -AsSecureString + PS> $cert = Get-PSBuildCertificate -CertificateSource PfxFile -PfxFilePath './codesign.pfx' -PfxFilePassword $pass + + Load a code-signing certificate from a PFX file on disk. + #> + [CmdletBinding()] + [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingPlainTextForPassword', + 'CertificatePasswordEnvVar', + Justification = 'This is not a password in plain text. It is the name of an environment variable that contains the password, which is a common pattern for CI/CD pipelines and secrets management.' + )] + param( + [ValidateSet('Auto', 'Store', 'Thumbprint', 'EnvVar', 'PfxFile')] + [string]$CertificateSource = 'Auto', + + [string]$CertStoreLocation = 'Cert:\CurrentUser\My', + + [string]$Thumbprint, + + [string]$CertificateEnvVar = 'SIGNCERTIFICATE', + + [string]$CertificatePasswordEnvVar = 'CERTIFICATEPASSWORD', + + [string]$PfxFilePath, + + [securestring]$PfxFilePassword, + + [switch]$SkipValidation + ) + + # Resolve 'Auto' to the actual source based on environment variable presence + $resolvedSource = $CertificateSource + if ($resolvedSource -eq 'Auto') { + $resolvedSource = if (-not [string]::IsNullOrEmpty([System.Environment]::GetEnvironmentVariable($CertificateEnvVar))) { + 'EnvVar' + } else { + 'Store' + } + Write-Verbose ($LocalizedData.CertificateSourceAutoResolved -f $resolvedSource) + } + + $cert = $null + + switch ($resolvedSource) { + 'Store' { + # Throw if running on a non-Windows platform since the certificate store is not supported. + # $IsWindows does not exist on Windows PowerShell 5.1 (Desktop edition), where it is $null + # and the platform is always Windows; only treat the platform as non-Windows when $IsWindows + # is explicitly $false (PowerShell 7+ on Linux/macOS). + if ($null -ne $IsWindows -and -not $IsWindows) { + throw $LocalizedData.CertificateSourceStoreNotSupported + } + $cert = Get-ChildItem -Path $CertStoreLocation -CodeSigningCert | + Where-Object { $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } | + Select-Object -First 1 + if ($cert) { + Write-Verbose ($LocalizedData.CertificateResolvedFromStore -f $CertStoreLocation, $cert.Subject) + } + } + 'Thumbprint' { + if ([string]::IsNullOrWhiteSpace($Thumbprint)) { + throw "CertificateSource 'Thumbprint' requires a non-empty Thumbprint value." + } + + # Normalize thumbprint input by removing whitespace for robust matching + $normalizedThumbprint = ($Thumbprint -replace '\s', '') + + $cert = Get-ChildItem -Path $CertStoreLocation -CodeSigningCert | + Where-Object { + ($_.Thumbprint -replace '\s', '') -ieq $normalizedThumbprint -and + $_.HasPrivateKey -and + $_.NotAfter -gt (Get-Date) + } | + Select-Object -First 1 + if ($cert) { + Write-Verbose ($LocalizedData.CertificateResolvedFromThumbprint -f $Thumbprint, $cert.Subject) + } + } + 'EnvVar' { + $b64Value = [System.Environment]::GetEnvironmentVariable($CertificateEnvVar) + if ([string]::IsNullOrWhiteSpace($b64Value)) { + throw "Environment variable '$CertificateEnvVar' is not set or is empty. When using CertificateSource='EnvVar', you must provide a Base64-encoded PFX in this variable." + } + + try { + $buffer = [System.Convert]::FromBase64String($b64Value) + } catch [System.FormatException] { + throw "Environment variable '$CertificateEnvVar' does not contain a valid Base64-encoded PFX value." + } + $password = [System.Environment]::GetEnvironmentVariable($CertificatePasswordEnvVar) + $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($buffer, $password) + Write-Verbose ($LocalizedData.CertificateResolvedFromEnvVar -f $CertificateEnvVar) + } + 'PfxFile' { + $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($PfxFilePath, $PfxFilePassword) + Write-Verbose ($LocalizedData.CertificateResolvedFromPfxFile -f $PfxFilePath) + } + } + + # Validate certificates loaded from EnvVar or PfxFile sources unless -SkipValidation is specified + if ($cert -and -not $SkipValidation -and ($resolvedSource -eq 'EnvVar' -or $resolvedSource -eq 'PfxFile')) { + # Check for private key + if (-not $cert.HasPrivateKey) { + throw ($LocalizedData.CertificateMissingPrivateKey -f $cert.Subject) + } + + # Check expiration + if ($cert.NotAfter -le (Get-Date)) { + throw ($LocalizedData.CertificateExpired -f $cert.NotAfter, $cert.Subject) + } + + # Check for Code Signing EKU (1.3.6.1.5.5.7.3.3) + $codeSigningOid = '1.3.6.1.5.5.7.3.3' + $hasCodeSigningEku = $cert.EnhancedKeyUsageList | Where-Object { $_.ObjectId -eq $codeSigningOid } + if (-not $hasCodeSigningEku) { + throw ($LocalizedData.CertificateMissingCodeSigningEku -f $cert.Subject) + } + + Write-Verbose "Certificate validation passed: HasPrivateKey=$($cert.HasPrivateKey), NotAfter=$($cert.NotAfter), CodeSigningEKU=Present" + } + + $certSubject = if ($cert) { $cert.Subject } else { 'No certificate found' } + Write-Verbose ('Certificate resolution complete: ' + $certSubject) + $cert +} diff --git a/PowerShellBuild/Public/Invoke-PSBuildModuleSigning.ps1 b/PowerShellBuild/Public/Invoke-PSBuildModuleSigning.ps1 new file mode 100644 index 0000000..58d9f24 --- /dev/null +++ b/PowerShellBuild/Public/Invoke-PSBuildModuleSigning.ps1 @@ -0,0 +1,88 @@ +function Invoke-PSBuildModuleSigning { + <# + .SYNOPSIS + Signs PowerShell module files with an Authenticode signature. + .DESCRIPTION + Signs all files matching the Include patterns found under Path using + Set-AuthenticodeSignature. Typically called after the module is staged to the output + directory and before the catalog file is created, so that all signed source files are + captured in the catalog hash. + + Authenticode signing is Windows-only. This function will fail on Linux or macOS. + + Use Get-PSBuildCertificate to resolve the certificate from any of the supported sources + (certificate store, PFX file, Base64 environment variable, thumbprint, etc.) before + calling this function. + .PARAMETER Path + The directory to search recursively for files to sign. Typically the module output + directory (PSBPreference.Build.ModuleOutDir). + .PARAMETER Certificate + The X509Certificate2 code-signing certificate to sign files with. Must have a private + key and an Extended Key Usage (EKU) of Code Signing (1.3.6.1.5.5.7.3.3). + .PARAMETER TimestampServer + RFC 3161 timestamp server URI to embed in the Authenticode signature, allowing the + signature to remain valid after the certificate expires. Default: http://timestamp.digicert.com. + + Other common timestamp servers: + http://timestamp.sectigo.com + http://timestamp.comodoca.com + http://tsa.starfieldtech.com + http://timestamp.globalsign.com/scripts/timstamp.dll + .PARAMETER HashAlgorithm + Hash algorithm for the Authenticode signature. + Valid values: SHA256 (default), SHA384, SHA512, SHA1. + SHA1 is deprecated; prefer SHA256 or higher. + .PARAMETER Include + Glob patterns of file names to sign. Searched recursively under Path. + Default: *.psd1, *.psm1, *.ps1. + .OUTPUTS + System.Management.Automation.Signature + Returns the Signature objects from Set-AuthenticodeSignature for each signed file. + .EXAMPLE + PS> $cert = Get-PSBuildCertificate + PS> Invoke-PSBuildModuleSigning -Path .\Output\MyModule\1.0.0 -Certificate $cert + + Sign all .psd1, .psm1, and .ps1 files in the module output directory using a + certificate resolved automatically from the environment or certificate store. + .EXAMPLE + PS> $cert = Get-PSBuildCertificate -CertificateSource Thumbprint -Thumbprint 'AB12CD...' + PS> Invoke-PSBuildModuleSigning -Path .\Output\MyModule\1.0.0 -Certificate $cert ` + -TimestampServer 'http://timestamp.sectigo.com' -Include '*.psd1','*.psm1' + + Sign only the manifest and root module using a specific certificate and a custom + timestamp server. + #> + [CmdletBinding()] + [OutputType([System.Management.Automation.Signature])] + param( + [parameter(Mandatory)] + [ValidateScript({ + if (-not (Test-Path -Path $_ -PathType Container)) { + throw ($LocalizedData.PathArgumentMustBeAFolder) + } + $true + })] + [string]$Path, + + [parameter(Mandatory)] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, + + [string]$TimestampServer = 'http://timestamp.digicert.com', + + [ValidateSet('SHA256', 'SHA384', 'SHA512', 'SHA1')] + [string]$HashAlgorithm = 'SHA256', + + [string[]]$Include = @('*.psd1', '*.psm1', '*.ps1') + ) + + $files = Get-ChildItem -Path $Path -Recurse -Include $Include + Write-Verbose ($LocalizedData.SigningModuleFiles -f $files.Count, ($Include -join ', '), $Path) + + $sigParams = @{ + Certificate = $Certificate + TimestampServer = $TimestampServer + HashAlgorithm = $HashAlgorithm + } + + $files | Set-AuthenticodeSignature @sigParams +} diff --git a/PowerShellBuild/Public/New-PSBuildFileCatalog.ps1 b/PowerShellBuild/Public/New-PSBuildFileCatalog.ps1 new file mode 100644 index 0000000..d8915e6 --- /dev/null +++ b/PowerShellBuild/Public/New-PSBuildFileCatalog.ps1 @@ -0,0 +1,73 @@ +function New-PSBuildFileCatalog { + <# + .SYNOPSIS + Creates a Windows catalog (.cat) file for a PowerShell module. + .DESCRIPTION + Wraps New-FileCatalog to generate a catalog file that records cryptographic hashes of + all files in the module output directory. The catalog can later be signed with + Invoke-PSBuildModuleSigning (or Set-AuthenticodeSignature) to provide tamper detection + and a trust chain for the entire module. + + The recommended signing order is: + 1. Sign module files (*.psd1, *.psm1, *.ps1) with Invoke-PSBuildModuleSigning. + 2. Create the catalog with New-PSBuildFileCatalog (hashes already-signed files). + 3. Sign the catalog file with Invoke-PSBuildModuleSigning -Include '*.cat'. + + Catalog file creation requires Windows (New-FileCatalog is not available on Linux or macOS). + + Reference: https://p0w3rsh3ll.wordpress.com/2017/09/19/psgallery-and-catalog-files/ + .PARAMETER ModulePath + The directory whose contents will be hashed and recorded in the catalog. + Typically the module output directory (PSBPreference.Build.ModuleOutDir). + .PARAMETER CatalogFilePath + The full path (directory + filename) of the .cat file to create. + By convention this is '\.cat'. + .PARAMETER CatalogVersion + The catalog hash version. + 1 = SHA1, compatible with Windows 7 and Windows Server 2008 R2. + 2 = SHA2 (SHA-256), required for Windows 8 / Server 2012 and newer. Default: 2. + .OUTPUTS + System.IO.FileInfo + Returns the FileInfo object of the created catalog file. + .EXAMPLE + PS> New-PSBuildFileCatalog -ModulePath .\Output\MyModule\1.0.0 ` + -CatalogFilePath .\Output\MyModule\1.0.0\MyModule.cat + + Create a version-2 (SHA2) catalog for all files in the module output directory. + .EXAMPLE + PS> New-PSBuildFileCatalog -ModulePath .\Output\MyModule\1.0.0 ` + -CatalogFilePath .\Output\MyModule\1.0.0\MyModule.cat -CatalogVersion 1 + + Create a SHA1 (version 1) catalog for compatibility with Windows 7 / Server 2008 R2. + #> + [CmdletBinding()] + [OutputType([System.IO.FileInfo])] + param( + [parameter(Mandatory)] + [ValidateScript({ + if (-not (Test-Path -Path $_ -PathType Container)) { + throw ($LocalizedData.PathArgumentMustBeAFolder) + } + $true + })] + [string]$ModulePath, + + [parameter(Mandatory)] + [string]$CatalogFilePath, + + [ValidateRange(1, 2)] + [int]$CatalogVersion = 2 + ) + + Write-Verbose ($LocalizedData.CreatingFileCatalog -f $CatalogFilePath, $CatalogVersion) + + $catalogParams = @{ + Path = $ModulePath + CatalogFilePath = $CatalogFilePath + CatalogVersion = $CatalogVersion + } + + Microsoft.PowerShell.Security\New-FileCatalog @catalogParams + + Write-Verbose ($LocalizedData.FileCatalogCreated -f $CatalogFilePath) +} diff --git a/PowerShellBuild/build.properties.ps1 b/PowerShellBuild/build.properties.ps1 index d245ec3..2f1c0b0 100644 --- a/PowerShellBuild/build.properties.ps1 +++ b/PowerShellBuild/build.properties.ps1 @@ -144,13 +144,75 @@ $moduleVersion = (Import-PowerShellDataFile -Path $env:BHPSModuleManifest).Modul # Credential to authenticate to PowerShell repository with PSRepositoryCredential = $null } + Sign = @{ + # Enable/disable Authenticode signing of module files. Must be $true for any + # signing or catalog tasks to execute. + Enabled = $false + + # Certificate source used to resolve the code-signing certificate. + # Valid values: + # Auto - Uses EnvVar if CertificateEnvVar is populated, otherwise falls back to Store. + # This is the recommended setting for pipelines that share a common psakeFile. + # Store - Selects the first valid, unexpired code-signing certificate with a private + # key from the Windows certificate store (CertStoreLocation). + # Thumbprint - Like Store, but selects a specific certificate by Thumbprint. + # EnvVar - Decodes a Base64-encoded PFX from the CertificateEnvVar environment + # variable. Common in GitHub Actions, Azure DevOps, and GitLab CI. + # PfxFile - Loads a PFX/P12 file from PfxFilePath with an optional PfxFilePassword. + CertificateSource = 'Auto' + + # Windows certificate store path searched by Store and Thumbprint sources. + CertStoreLocation = 'Cert:\CurrentUser\My' + + # Specific certificate thumbprint to select (Thumbprint source only). + Thumbprint = $null + + # Name of the environment variable that holds the Base64-encoded PFX certificate. + # Used by the EnvVar source and as the presence-detection key for Auto. + CertificateEnvVar = 'SIGNCERTIFICATE' + + # Name of the environment variable that holds the PFX password (EnvVar source). + CertificatePasswordEnvVar = 'CERTIFICATEPASSWORD' + + # File system path to a PFX/P12 certificate file (PfxFile source). + PfxFilePath = $null + + # Password for the PFX file as a SecureString (PfxFile source). + PfxFilePassword = $null + + # A pre-resolved [System.Security.Cryptography.X509Certificates.X509Certificate2] object. + # When set, CertificateSource is ignored and this certificate is used directly. + # Useful for Azure Key Vault, HSM, or other custom certificate providers. + Certificate = $null + + # When true and using the Store or Thumbprint sources, skip the + # certificate validity check that ensures the certificate is not expired + # and has a private key. This is not recommended for production use but + # can be useful in CI environments where certificates are frequently + # renewed and updated. + SkipCertificateValidation = $false + + # RFC 3161 timestamp server URI embedded in Authenticode signatures. + TimestampServer = 'http://timestamp.digicert.com' + + # Authenticode hash algorithm. Valid values: SHA256, SHA384, SHA512, SHA1. + HashAlgorithm = 'SHA256' + + # Glob patterns of files to sign in the module output directory. + FilesToSign = @('*.psd1', '*.psm1', '*.ps1') + + Catalog = @{ + # Enable/disable Windows catalog (.cat) file creation and signing. + # Requires Sign.Enabled = $true. + Enabled = $false + + # Catalog hash version. + # 1 = SHA1, compatible with Windows 7 and Windows Server 2008 R2. + # 2 = SHA2, required for Windows 8 / Server 2012 and newer. + Version = 2 + + # Catalog file name. Defaults to '.cat' when $null. + FileName = $null + } + } } - -# Enable/disable generation of a catalog (.cat) file for the module. -# [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] -# $catalogGenerationEnabled = $true - -# # Select the hash version to use for the catalog file: 1 for SHA1 (compat with Windows 7 and -# # Windows Server 2008 R2), 2 for SHA2 to support only newer Windows versions. -# [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] -# $catalogVersion = 2 diff --git a/PowerShellBuild/en-US/Messages.psd1 b/PowerShellBuild/en-US/Messages.psd1 index 3662452..58aff5e 100644 --- a/PowerShellBuild/en-US/Messages.psd1 +++ b/PowerShellBuild/en-US/Messages.psd1 @@ -23,4 +23,17 @@ PSScriptAnalyzerResults=PSScriptAnalyzer results: ScriptAnalyzerErrors=One or more ScriptAnalyzer errors were found! ScriptAnalyzerWarnings=One or more ScriptAnalyzer warnings were found! ScriptAnalyzerIssues=One or more ScriptAnalyzer issues were found! +NoCertificateFound=No valid code signing certificate was found. Verify the configured CertificateSource and that a certificate with a private key is available. +CertificateResolvedFromStore=Resolved code signing certificate from store [{0}]: Subject=[{1}] +CertificateResolvedFromThumbprint=Resolved code signing certificate by thumbprint [{0}]: Subject=[{1}] +CertificateResolvedFromEnvVar=Resolved code signing certificate from environment variable [{0}] +CertificateResolvedFromPfxFile=Resolved code signing certificate from PFX file [{0}] +SigningModuleFiles=Signing [{0}] file(s) matching [{1}] in [{2}]... +CreatingFileCatalog=Creating file catalog [{0}] (version {1})... +FileCatalogCreated=File catalog created: [{0}] +CertificateSourceAutoResolved=CertificateSource is 'Auto'. Resolved to '{0}'. +CertificateMissingPrivateKey=The resolved certificate does not have an accessible private key. Code signing requires a certificate with a private key. Subject=[{0}] +CertificateExpired=The resolved certificate has expired (NotAfter: {0}). Code signing requires a valid, unexpired certificate. Subject=[{1}] +CertificateMissingCodeSigningEku=The resolved certificate does not have the Code Signing Enhanced Key Usage (EKU: 1.3.6.1.5.5.7.3.3). Subject=[{0}] +CertificateSourceStoreNotSupported=CertificateSource 'Store' is only supported on Windows platforms. '@ diff --git a/PowerShellBuild/psakeFile.ps1 b/PowerShellBuild/psakeFile.ps1 index c1be2a6..cff79f9 100644 --- a/PowerShellBuild/psakeFile.ps1 +++ b/PowerShellBuild/psakeFile.ps1 @@ -3,7 +3,14 @@ Remove-Variable -Name PSBPreference -Scope Script -Force -ErrorAction Ignore Set-Variable -Name PSBPreference -Option ReadOnly -Scope Script -Value (. ([IO.Path]::Combine($PSScriptRoot, 'build.properties.ps1'))) -Properties {} +Properties { + $importLocalizedDataSplat = @{ + BindingVariable = 'LocalizedData' + FileName = 'Messages.psd1' + ErrorAction = 'SilentlyContinue' + } + Import-LocalizedData @importLocalizedDataSplat +} FormatTaskName { param($taskName) @@ -45,6 +52,18 @@ if ($null -eq $PSBGenerateUpdatableHelpDependency) { if ($null -eq $PSBPublishDependency) { $PSBPublishDependency = @('Test') } +if ($null -eq $PSBSignModuleDependency) { + $PSBSignModuleDependency = @('Build') +} +if ($null -eq $PSBBuildCatalogDependency) { + $PSBBuildCatalogDependency = @('SignModule') +} +if ($null -eq $PSBSignCatalogDependency) { + $PSBSignCatalogDependency = @('BuildCatalog') +} +if ($null -eq $PSBSignDependency) { + $PSBSignDependency = @('SignCatalog') +} #endregion Task Dependencies # This psake file is meant to be referenced from another @@ -52,11 +71,11 @@ if ($null -eq $PSBPublishDependency) { # Task default -depends Test Task Init { - Initialize-PSBuild -UseBuildHelpers -BuildEnvironment $PSBPreference + Initialize-PSBuild -UseBuildHelpers -BuildEnvironment $PSBPreference -Verbose:($VerbosePreference -eq 'Continue') } -Description 'Initialize build environment variables' Task Clean -Depends $PSBCleanDependency { - Clear-PSBuildOutputFolder -Path $PSBPreference.Build.ModuleOutDir + Clear-PSBuildOutputFolder -Path $PSBPreference.Build.ModuleOutDir -Verbose:($VerbosePreference -eq 'Continue') } -Description 'Clears module output directory' Task StageFiles -Depends $PSBStageFilesDependency { @@ -86,7 +105,7 @@ Task StageFiles -Depends $PSBStageFilesDependency { } } - Build-PSBuildModule @buildParams + Build-PSBuildModule @buildParams -Verbose:($VerbosePreference -eq 'Continue') } -Description 'Builds module based on source directory' Task Build -Depends $PSBBuildDependency -Description 'Builds module and generate help documentation' @@ -109,7 +128,7 @@ Task Analyze -Depends $PSBAnalyzeDependency -PreCondition $analyzePreReqs { SeverityThreshold = $PSBPreference.Test.ScriptAnalysis.FailBuildOnSeverityLevel SettingsPath = $PSBPreference.Test.ScriptAnalysis.SettingsPath } - Test-PSBuildScriptAnalysis @analyzeParams + Test-PSBuildScriptAnalysis @analyzeParams -Verbose:($VerbosePreference -eq 'Continue') } -Description 'Execute PSScriptAnalyzer tests' $pesterPreReqs = { @@ -143,6 +162,7 @@ Task Pester -Depends $PSBPesterDependency -PreCondition $pesterPreReqs { ImportModule = $PSBPreference.Test.ImportModule SkipRemainingOnFailure = $PSBPreference.Test.SkipRemainingOnFailure OutputVerbosity = $PSBPreference.Test.OutputVerbosity + Verbose = $VerbosePreference -eq 'Continue' } Test-PSBuildPester @pesterParams } -Description 'Execute Pester tests' @@ -170,6 +190,7 @@ Task GenerateMarkdown -Depends $PSBGenerateMarkdownDependency -PreCondition $gen AlphabeticParamsOrder = $PSBPreference.Docs.AlphabeticParamsOrder ExcludeDontShow = $PSBPreference.Docs.ExcludeDontShow UseFullTypeName = $PSBPreference.Docs.UseFullTypeName + Verbose = $VerbosePreference -eq 'Continue' } Build-PSBuildMarkdown @buildMDParams } -Description 'Generates PlatyPS markdown files from module help' @@ -183,7 +204,7 @@ $genHelpFilesPreReqs = { $result } Task GenerateMAML -Depends $PSBGenerateMAMLDependency -PreCondition $genHelpFilesPreReqs { - Build-PSBuildMAMLHelp -Path $PSBPreference.Docs.RootDir -DestinationPath $PSBPreference.Build.ModuleOutDir + Build-PSBuildMAMLHelp -Path $PSBPreference.Docs.RootDir -DestinationPath $PSBPreference.Build.ModuleOutDir -Verbose:($VerbosePreference -eq 'Continue') } -Description 'Generates MAML-based help from PlatyPS markdown files' $genUpdatableHelpPreReqs = { @@ -195,7 +216,7 @@ $genUpdatableHelpPreReqs = { $result } Task GenerateUpdatableHelp -Depends $PSBGenerateUpdatableHelpDependency -PreCondition $genUpdatableHelpPreReqs { - Build-PSBuildUpdatableHelp -DocsPath $PSBPreference.Docs.RootDir -OutputPath $PSBPreference.Help.UpdatableHelpOutDir + Build-PSBuildUpdatableHelp -DocsPath $PSBPreference.Docs.RootDir -OutputPath $PSBPreference.Help.UpdatableHelpOutDir -Verbose:($VerbosePreference -eq 'Continue') } -Description 'Create updatable help .cab file based on PlatyPS markdown help' Task Publish -Depends $PSBPublishDependency { @@ -218,6 +239,143 @@ Task Publish -Depends $PSBPublishDependency { Publish-PSBuildModule @publishParams } -Description 'Publish module to the defined PowerShell repository' +$signModulePreReqs = { + $result = $true + if (-not $PSBPreference.Sign.Enabled) { + Write-Warning 'Module signing is not enabled.' + $result = $false + } + if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) { + Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.' + $result = $false + } + $result +} +Task SignModule -Depends $PSBSignModuleDependency -PreCondition $signModulePreReqs { + $certParams = @{ + CertificateSource = $PSBPreference.Sign.CertificateSource + CertStoreLocation = $PSBPreference.Sign.CertStoreLocation + CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar + CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar + SkipValidation = $PSBPreference.Sign.SkipCertificateValidation + Verbose = $VerbosePreference -eq 'Continue' + } + if ($PSBPreference.Sign.Thumbprint) { + $certParams.Thumbprint = $PSBPreference.Sign.Thumbprint + } + if ($PSBPreference.Sign.PfxFilePath) { + $certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath + } + if ($PSBPreference.Sign.PfxFilePassword) { + $certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword + } + + $certificate = if ($PSBPreference.Sign.Certificate) { + $PSBPreference.Sign.Certificate + } else { + Get-PSBuildCertificate @certParams + } + + Assert ($null -ne $certificate) $LocalizedData.NoCertificateFound + + $signingParams = @{ + Path = $PSBPreference.Build.ModuleOutDir + Certificate = $certificate + TimestampServer = $PSBPreference.Sign.TimestampServer + HashAlgorithm = $PSBPreference.Sign.HashAlgorithm + Include = $PSBPreference.Sign.FilesToSign + Verbose = $VerbosePreference -eq 'Continue' + } + Invoke-PSBuildModuleSigning @signingParams +} -Description 'Signs module files (*.psd1, *.psm1, *.ps1) with an Authenticode signature' + +$buildCatalogPreReqs = { + $result = $true + if (-not ($PSBPreference.Sign.Enabled -and $PSBPreference.Sign.Catalog.Enabled)) { + Write-Warning 'Catalog generation is not enabled.' + $result = $false + } + if (-not (Get-Command -Name 'New-FileCatalog' -ErrorAction Ignore)) { + Write-Warning 'New-FileCatalog is not available. Catalog generation requires Windows.' + $result = $false + } + $result +} +Task BuildCatalog -Depends $PSBBuildCatalogDependency -PreCondition $buildCatalogPreReqs { + $catalogFileName = if ($PSBPreference.Sign.Catalog.FileName) { + $PSBPreference.Sign.Catalog.FileName + } else { + "$($PSBPreference.General.ModuleName).cat" + } + $catalogFilePath = Join-Path -Path $PSBPreference.Build.ModuleOutDir -ChildPath $catalogFileName + + $catalogParams = @{ + ModulePath = $PSBPreference.Build.ModuleOutDir + CatalogFilePath = $catalogFilePath + CatalogVersion = $PSBPreference.Sign.Catalog.Version + Verbose = $VerbosePreference -eq 'Continue' + } + New-PSBuildFileCatalog @catalogParams +} -Description 'Creates a Windows catalog (.cat) file for the built module' + +$signCatalogPreReqs = { + $result = $true + if (-not ($PSBPreference.Sign.Enabled -and $PSBPreference.Sign.Catalog.Enabled)) { + Write-Warning 'Catalog signing is not enabled.' + $result = $false + } + if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) { + Write-Warning 'Set-AuthenticodeSignature is not available. Catalog signing requires Windows.' + $result = $false + } + $result +} +Task SignCatalog -Depends $PSBSignCatalogDependency -PreCondition $signCatalogPreReqs { + $certParams = @{ + CertificateSource = $PSBPreference.Sign.CertificateSource + CertStoreLocation = $PSBPreference.Sign.CertStoreLocation + CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar + CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar + SkipValidation = $PSBPreference.Sign.SkipCertificateValidation + Verbose = $VerbosePreference -eq 'Continue' + } + if ($PSBPreference.Sign.Thumbprint) { + $certParams.Thumbprint = $PSBPreference.Sign.Thumbprint + } + if ($PSBPreference.Sign.PfxFilePath) { + $certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath + } + if ($PSBPreference.Sign.PfxFilePassword) { + $certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword + } + + $certificate = if ($PSBPreference.Sign.Certificate) { + $PSBPreference.Sign.Certificate + } else { + Get-PSBuildCertificate @certParams + } + + Assert ($null -ne $certificate) $LocalizedData.NoCertificateFound + + $catalogFileName = if ($PSBPreference.Sign.Catalog.FileName) { + $PSBPreference.Sign.Catalog.FileName + } else { + "$($PSBPreference.General.ModuleName).cat" + } + + $signingParams = @{ + Path = $PSBPreference.Build.ModuleOutDir + Certificate = $certificate + TimestampServer = $PSBPreference.Sign.TimestampServer + HashAlgorithm = $PSBPreference.Sign.HashAlgorithm + Include = @($catalogFileName) + Verbose = $VerbosePreference -eq 'Continue' + } + Invoke-PSBuildModuleSigning @signingParams +} -Description 'Signs the module catalog (.cat) file with an Authenticode signature' + +Task Sign -Depends $PSBSignDependency {} -Description 'Signs module files and catalog (meta task)' + Task ? -Description 'Lists the available tasks' { 'Available tasks:' $psake.context.Peek().Tasks.Keys | Sort-Object diff --git a/README.md b/README.md index 8725468..05c2e42 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ | GitHub Actions | PS Gallery | License | |-------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|--------------------------------------| -| [![GitHub Actions Status][github-actions-badge]][github-actions-build] [![GitHub Actions Status][github-actions-badge-publish]][github-actions-build] | [![PowerShell Gallery][psgallery-badge]][psgallery] | [![License][license-badge]][license] | +| [![GitHub Actions Status][github-actions-badge]][github-actions-build] [![GitHub Actions Status][github-actions-badge-publish]][github-actions-build] | [![PowerShell Gallery][psgallery-badge]][psgallery] [![PowerShell Gallery][psgallery-version]][psgallery] [![PowerShell Gallery][psgallery-platforms]][psgallery] | [![License][license-badge]][license] | This project aims to provide common [psake](https://github.com/psake/psake) and [Invoke-Build](https://github.com/nightroman/Invoke-Build) tasks for building, @@ -203,9 +203,11 @@ $PSBPreference.Test.CodeCoverage.Enabled = $false ![Example](./media/ib_example.png) [github-actions-badge]: https://github.com/psake/PowerShellBuild/actions/workflows/test.yml/badge.svg -[github-actions-badge-publish]: https://github.com/psake/PowerShellBuild/actions/workflows/publish.yaml/badge.svg +[github-actions-badge-publish]: https://github.com/psake/PowerShellBuild/actions/workflows/publish.yaml/badge.svg?event=release [github-actions-build]: https://github.com/psake/PowerShellBuild/actions -[psgallery-badge]: https://img.shields.io/powershellgallery/dt/powershellbuild.svg +[psgallery-badge]: https://img.shields.io/powershellgallery/dt/powershellbuild +[psgallery-version]: https://img.shields.io/powershellgallery/v/ChocoLogParse?label=version +[psgallery-platforms]: https://img.shields.io/powershellgallery/p/ChocoLogParse [psgallery]: https://www.powershellgallery.com/packages/PowerShellBuild [license-badge]: https://img.shields.io/github/license/psake/PowerShellBuild.svg [license]: https://raw.githubusercontent.com/psake/PowerShellBuild/main/LICENSE diff --git a/aim.config.json b/aim.config.json new file mode 100644 index 0000000..620e754 --- /dev/null +++ b/aim.config.json @@ -0,0 +1,31 @@ +{ + "version": "latest", + "modules": { + "include": [ + "agent-workflow", + "shorthand", + "git-workflow", + "testing", + "powershell", + "markdown", + "readme", + "github-cli", + "releases", + "contributing", + "update", + "repository-specific" + ], + "exclude": [] + }, + "externalSources": { + "enabled": true, + "repositories": [ + { + "name": "awesome-copilot", + "url": "https://github.com/github/awesome-copilot", + "path": "instructions", + "description": "Community-contributed instructions from GitHub's awesome-copilot repository" + } + ] + } +} diff --git a/instructions/agent-workflow.instructions.md b/instructions/agent-workflow.instructions.md new file mode 100644 index 0000000..efc3f66 --- /dev/null +++ b/instructions/agent-workflow.instructions.md @@ -0,0 +1,96 @@ +--- +applyTo: '**/*' +description: 'Mandatory pre-flight protocol for AI agents' +--- + +# Agent Workflow Instructions + +## Purpose + +This file defines the recommended workflow that AI agents should follow when working in +repositories using AIM. It ensures agents understand the context and guidelines before +starting work. + +## Pre-Flight Protocol + +**Before starting any task, AI agents should:** + +### 1. Identify Task Type + +Analyze the user's request and identify all areas it touches. Common patterns: + +- Code development (specific languages or frameworks) +- Documentation (Markdown files, README files) +- Git operations (commits, branches, PRs) +- Testing and quality assurance +- Security considerations +- Repository-specific customizations + +### 2. Consider Applicable Instructions + +Review the instruction files listed in the repository's `AGENTS.md` to understand: + +- Language-specific coding standards +- Framework conventions +- Documentation requirements +- Git workflow expectations +- Security best practices + +### 3. Implement with Compliance + +Execute your task following the guidelines from the applicable instruction sections. + +## Best Practices + +### Read Before Writing + +- Always read existing code before modifying it +- Understand the project's patterns and conventions +- Check for existing implementations before creating new ones + +### Confirm Understanding + +When starting complex tasks, briefly confirm your understanding: + +> "Based on the instructions, I'll follow [specific guidelines]. Here's my approach..." + +This builds trust and catches misunderstandings early. + +### Avoid Over-Engineering + +- Only make changes that are directly requested +- Keep solutions simple and focused +- Don't add features, refactoring, or improvements beyond what was asked + +### Security First + +- Never introduce security vulnerabilities +- Be careful with user input validation +- Avoid hardcoding secrets or credentials +- Follow the security guidelines in this document + +## When in Doubt + +1. **Ask for clarification** - Better to ask than implement incorrectly +2. **Check existing code** - Follow established patterns in the codebase +3. **Keep it simple** - The simplest solution that works is usually best + +## Post-Task Protocol + +### Before Committing + +1. **Run tests** - Ensure all tests pass before committing +2. **Check repository-specific requirements** - Review `repository-specific.instructions.md` for + any post-task requirements such as: + - Release processes (version bumps, changelogs, tags) + - Commit message conventions beyond standard guidelines + - Required reviewers or approval workflows + - Documentation updates + +Following repository-specific requirements ensures consistency with the project's established +workflows and prevents incomplete changes from being committed. + +## Custom Instructions + +If this repository has a custom instructions section, those guidelines take precedence for +repository-specific conventions and may override or supplement the general instructions above. diff --git a/instructions/contributing.instructions.md b/instructions/contributing.instructions.md new file mode 100644 index 0000000..3c03a57 --- /dev/null +++ b/instructions/contributing.instructions.md @@ -0,0 +1,157 @@ +--- +applyTo: '**/*' +description: 'Guidelines for contributing improvements back to the upstream AIM repository' +--- + +# Contributing Instructions for AI Agents + +When users want to improve, fix, or extend the AI agent instructions, this guide helps agents +facilitate contributions back to the upstream AIM repository. + +## When to Contribute Upstream vs. Modify Locally + +### Contribute Upstream (Submit a PR) + +- Fixing errors or typos in instruction files +- Clarifying confusing or ambiguous instructions +- Adding missing best practices that benefit all users +- Creating new instruction modules for languages, frameworks, or tools +- Improving examples or adding helpful code snippets + +### Modify Locally Only + +- Organization-specific conventions or standards +- Project-specific customizations +- Internal tooling or proprietary workflows +- Content that references internal systems or URLs + +**Local changes belong in `repository-specific.instructions.md`** - this file is never synced from upstream. + +## Agent-Assisted Contribution Workflow + +When a user wants to contribute to upstream, guide them through these steps: + +### 1. Fork the Repository + +```bash +gh repo fork tablackburn/ai-agent-instruction-modules --clone +cd ai-agent-instruction-modules +``` + +### 2. Create a Feature Branch + +```bash +git checkout -b feature/descriptive-branch-name +``` + +Use descriptive branch names: + +- `feature/add-python-module` - New module +- `fix/powershell-typo` - Bug fix +- `docs/clarify-update-procedure` - Documentation improvement + +### 3. Make Changes + +Follow existing patterns in the repository: + +**For new instruction files:** + +- Place in `instruction-templates/` folder +- Use `.instructions.md` extension +- Include required YAML frontmatter + +**For existing files:** + +- Preserve the file's structure and style +- Make minimal, focused changes +- Don't introduce unrelated modifications + +### 4. Validate Changes + +```powershell +Invoke-Pester -Path .\tests\ +``` + +Ensure all tests pass before committing. + +### 5. Commit with Conventional Commits + +```bash +git commit -m "feat: Add Python type hints module" +``` + +Prefixes: + +- `feat:` - New feature or module +- `fix:` - Bug fix or correction +- `docs:` - Documentation only +- `refactor:` - Code restructuring without behavior change + +### 6. Push and Create Pull Request + +```bash +git push origin feature/descriptive-branch-name +gh pr create --title "feat: Add Python type hints module" --body "Description of changes" +``` + +## Module Requirements + +All instruction files must include YAML frontmatter: + +```yaml +--- +applyTo: '**/*.py' +description: 'Brief description of what this module covers' +--- +``` + +**Frontmatter fields:** + +- `applyTo` - Glob pattern for applicable files (e.g., `'**/*'`, `'**/*.py'`, `'**/README.md'`) +- `description` - One-line description of the module's purpose + +**Content guidelines:** + +- Keep instructions generic and universally applicable +- Use placeholder examples (``, ``, `example.com`) +- Avoid organization-specific references +- Include practical code examples where helpful +- Follow markdown conventions from `markdown.instructions.md` + +## Pull Request Guidelines + +**Title:** Use conventional commit format (e.g., `feat: Add Python module`) + +**Description should include:** + +- Summary of changes (1-3 bullet points) +- Motivation or problem being solved +- Any breaking changes or migration notes + +**Example PR body:** + +```markdown +## Summary + +- Add Python type hints and docstring guidelines +- Include examples for common patterns +- Reference PEP 484 and PEP 257 standards + +## Motivation + +Python developers need consistent guidance on type annotations and documentation strings. +``` + +## After Submission + +- Respond to review feedback promptly +- Make requested changes in additional commits +- Once merged, downstream repositories can sync using `update.instructions.md` + +## Questions or Discussion + +For questions about contributing, open an issue on the upstream repository: + +```bash +gh issue create --repo tablackburn/ai-agent-instruction-modules --title "Question: Your topic" --body "Your question here" +``` diff --git a/instructions/git-workflow.instructions.md b/instructions/git-workflow.instructions.md new file mode 100644 index 0000000..9c3d96c --- /dev/null +++ b/instructions/git-workflow.instructions.md @@ -0,0 +1,280 @@ +--- +applyTo: '**/*' +description: 'Git workflow conventions including branching, commits, and pull requests' +--- + +# Git Workflow Instructions + +Guidelines for consistent Git usage across repositories. + +## Working on Branches + +**Agents must always work on branches, never directly on main.** + +Before starting any work: + +1. Create a branch from `main` using the naming conventions below +2. Make changes in small, logical commits +3. Push the branch and create a pull request +4. Wait for CI checks and address any review feedback +5. Report status and wait for instructions before merging + +This ensures all changes go through review and CI validation before reaching the main branch. + +## Branch Naming + +Use descriptive, lowercase branch names with hyphens. + +### Basic Format + +```text +/ +``` + +### Format with Ticket Numbers + +When using project management tools, include the ticket identifier: + +```text +/- +``` + +### Branch Types + +| Prefix | Purpose | Example | +| ----------- | ------------------------------------ | ------------------------------------ | +| `feature/` | New functionality | `feature/user-authentication` | +| `bugfix/` | Bug fixes | `bugfix/login-validation-error` | +| `hotfix/` | Urgent production patches | `hotfix/security-vulnerability` | +| `release/` | Release preparation | `release/v1.2.0` | +| `docs/` | Documentation only | `docs/api-documentation` | +| `refactor/` | Code restructuring | `refactor/database-queries` | +| `test/` | Adding or updating tests | `test/payment-integration` | +| `chore/` | Maintenance tasks | `chore/update-dependencies` | + +### Examples with Ticket Numbers + +```text +feature/PROJ-123-add-user-authentication +bugfix/PROJ-456-fix-login-validation +hotfix/PROJ-789-patch-security-issue +``` + +### Best Practices + +- **Be descriptive**: Names should reflect the branch's purpose or task +- **Be concise**: Keep names brief but meaningful +- **Be consistent**: Follow the same conventions across the team +- **Use lowercase**: Avoid mixed case for cross-platform compatibility +- **Use hyphens**: Separate words with hyphens, not underscores or spaces + +### Technical Constraints + +Avoid the following in branch names: + +- Dots at the start of the name +- Trailing slashes +- Reserved Git names (`HEAD`, `FETCH_HEAD`) +- Spaces or special characters (except hyphens and forward slashes) + +### Avoid + +- Overly long names +- Generic names like `fix`, `update`, `changes` +- Names without context or purpose + +## Commit Messages + +Follow [Conventional Commits](https://www.conventionalcommits.org/) format: + +```text +: + +[optional body] + +[optional footer] +``` + +**Types:** + +- `feat:` - New feature +- `fix:` - Bug fix +- `docs:` - Documentation changes +- `style:` - Formatting (no code change) +- `refactor:` - Code restructuring +- `test:` - Adding/updating tests +- `chore:` - Maintenance tasks + +**Guidelines:** + +- Use imperative mood ("Add feature" not "Added feature") +- Keep first line under 72 characters +- Capitalize first letter after type +- No period at end of subject line +- Separate subject from body with blank line + +**Good examples:** + +```text +feat: Add user authentication flow +fix: Resolve null reference in payment processing +docs: Update API endpoint documentation +refactor: Extract validation logic to separate module +``` + +**Avoid:** + +```text +Fixed stuff +WIP +updates +asdfasdf +``` + +## Pull Request Guidelines + +### Before Creating a PR + +1. Ensure your branch is up to date with the base branch +2. Run tests locally and verify they pass +3. Review your own changes first +4. Remove debugging code and console logs + +### PR Title + +Use the same format as commit messages: + +```text +feat: Add user authentication flow +``` + +### PR Description + +Include: + +- **Summary** - What changed and why (1-3 bullet points) +- **Test plan** - How to verify the changes work +- **Breaking changes** - Note any breaking changes + +**Template:** + +```markdown +## Summary + +- Added user login and logout functionality +- Integrated with OAuth2 provider +- Added session management + +## Test Plan + +- [ ] Login with valid credentials succeeds +- [ ] Login with invalid credentials shows error +- [ ] Logout clears session + +## Breaking Changes + +None +``` + +### PR Size + +- Keep PRs focused and small when possible +- Large changes should be split into logical commits +- If a PR is too large, consider breaking it into smaller PRs + +### After Creating a PR + +1. **Monitor CI**: Wait for CI checks to complete and verify they pass +2. **Check for comments**: Review the PR for any feedback or requested changes +3. **Address feedback**: Make additional commits to address review comments +4. **Report status**: Report the PR status to the user and wait for instructions before merging + +## Branching Strategy + +### Default Branch + +- Use `main` as the default branch name for new repositories +- `main` is the industry standard and preferred for inclusive terminology +- When working with existing repositories using `master`, follow the repository's convention +- Consider migrating legacy repositories from `master` to `main` when practical + +### Main Branch + +- `main` is the production-ready branch +- Should always be in a deployable state +- Direct commits to main should be avoided + +### Feature Branches + +1. Create feature branch from `main` +2. Make changes in small, logical commits +3. Push branch and create PR +4. After review and approval, merge to `main` +5. Delete feature branch after merge + +### Keeping Branches Updated + +```bash +# Update your feature branch with latest main +git fetch origin +git rebase origin/main +``` + +Prefer rebase for feature branches to maintain clean history. + +## Merge Strategy + +### Squash and Merge (Recommended for feature branches) + +- Combines all commits into one clean commit +- Keeps main branch history clean +- Use when feature branch has many small/WIP commits + +### Merge Commit + +- Preserves full commit history +- Use for significant features where history is valuable +- Use for release branches + +### Rebase and Merge + +- Applies commits linearly without merge commit +- Use when commits are already clean and logical + +## Git Safety + +### Before Force Pushing + +- Never force push to `main` or shared branches +- Only force push to your own feature branches +- Always communicate with team before force pushing shared branches + +### Avoiding Common Issues + +- Pull before pushing to avoid conflicts +- Don't commit sensitive data (secrets, credentials, API keys) +- Use `.gitignore` for build artifacts and dependencies +- Review staged changes before committing + +## Useful Commands + +```bash +# View branch status +git status + +# View commit history +git log --oneline -10 + +# Amend last commit (before pushing) +git commit --amend + +# Stash changes temporarily +git stash +git stash pop + +# Undo last commit (keep changes) +git reset --soft HEAD~1 + +# View changes before committing +git diff --staged +``` diff --git a/instructions/github-cli.instructions.md b/instructions/github-cli.instructions.md new file mode 100644 index 0000000..347a024 --- /dev/null +++ b/instructions/github-cli.instructions.md @@ -0,0 +1,252 @@ +--- +applyTo: '**/*' +description: 'GitHub CLI usage guidelines and best practices (operational instructions for running gh commands, not file-specific)' +--- + +# GitHub CLI Guidelines + +Instructions for using GitHub CLI (`gh`) for repository operations. + +## Authentication + +Verify authentication before performing operations: + +```bash +# Check current authentication status +gh auth status + +# If not authenticated +gh auth login +``` + +## Repository Operations + +### Repository Discovery + +```bash +# List repositories in an organization +gh repo list --limit 100 + +# Get repository default branch (don't assume main) +gh api repos// --jq '.default_branch' + +# View repository details +gh repo view / +``` + +### Cloning and Forking + +```bash +# Clone a repository +gh repo clone / + +# Fork a repository +gh repo fork / --clone +``` + +## Issue Management + +### Creating Issues + +```bash +# Create a new issue +gh issue create --title "Issue Title" --body "Issue description" + +# Create with labels and assignee +gh issue create --title "Bug: Login fails" --body "Description" --label "bug" --assignee "@me" + +# Create interactively +gh issue create +``` + +### Viewing and Searching Issues + +```bash +# List open issues +gh issue list + +# View specific issue +gh issue view + +# Search issues +gh issue list --search "bug in:title" +``` + +### Issue Labels + +Common labels to use: + +- `bug` - Something isn't working +- `enhancement` - New feature or request +- `documentation` - Documentation improvements +- `question` - Further information requested +- `good first issue` - Good for newcomers + +## Pull Request Workflows + +### Creating Pull Requests + +```bash +# Create PR from current branch +gh pr create --title "PR Title" --body "Description" + +# Create draft PR +gh pr create --title "WIP: Feature" --body "Work in progress" --draft + +# Create PR with specific base branch +gh pr create --base develop --title "Feature" --body "Description" +``` + +### PR Review + +```bash +# List PRs awaiting review +gh pr list --search "review-requested:@me" + +# View PR details +gh pr view + +# Checkout PR locally +gh pr checkout + +# Approve PR +gh pr review --approve + +# Request changes +gh pr review --request-changes --body "Please fix..." +``` + +### Merging PRs + +```bash +# Merge PR +gh pr merge + +# Merge with squash +gh pr merge --squash + +# Merge with rebase +gh pr merge --rebase + +# Delete branch after merge +gh pr merge --delete-branch +``` + +## GitHub Actions + +### Workflow Management + +```bash +# List workflow runs +gh run list + +# View specific run +gh run view + +# Watch a running workflow +gh run watch + +# Re-run failed jobs +gh run rerun --failed +``` + +### Viewing Logs + +```bash +# View run logs +gh run view --log + +# View failed step logs +gh run view --log-failed +``` + +## Releases + +### Creating Releases + +Use `--notes-file` (write notes to a temporary file first) rather than `--notes` to avoid +escaping issues with backticks, backslashes, and quotes. For project releases, the rules in +`releases.instructions.md` take precedence over these examples. + +```bash +# Create release from tag (write notes to a file first to avoid escaping issues) +printf '## Highlights\n\n- Your release notes here\n' > release-notes.md +gh release create v1.0.0 --title "Version 1.0.0" --notes-file release-notes.md +rm release-notes.md + +# Create release with auto-generated notes +gh release create v1.0.0 --generate-notes + +# Create draft release +gh release create v1.0.0 --draft --title "Version 1.0.0" + +# Upload assets +gh release create v1.0.0 ./dist/*.zip --title "Version 1.0.0" +``` + +### Viewing Releases + +```bash +# List releases +gh release list + +# View latest release +gh release view --latest +``` + +## API Access + +### Direct API Calls + +```bash +# GET request +gh api repos// + +# POST request +gh api repos///issues --method POST -f title="Title" -f body="Body" + +# Use jq for filtering +gh api repos///pulls --jq '.[].title' +``` + +## Best Practices + +### Pre-Operation Validation + +```bash +# Verify you're in a git repository +gh repo view --json nameWithOwner --jq '.nameWithOwner' + +# Verify authentication +gh auth status +``` + +### Issue-to-Branch Workflow + +```bash +# Create issue and capture the issue number +ISSUE_NUM=$(gh issue create --title "Feature: New functionality" --body "Description" --json number --jq '.number') + +# Create feature branch using the captured issue number +git checkout -b "feature/issue-${ISSUE_NUM}-new-functionality" + +# Push and create PR +git push -u origin "feature/issue-${ISSUE_NUM}-new-functionality" +gh pr create --title "Feature: New functionality" --body "Closes #${ISSUE_NUM}" +``` + +### Common Flags + +- `--json` - Output as JSON +- `--jq` - Filter JSON output +- `--web` - Open in browser +- `--help` - Show help for any command + +## Environment Variables + +Useful environment variables: + +- `GH_TOKEN` - Authentication token +- `GH_HOST` - GitHub hostname (for enterprise) +- `GH_REPO` - Default repository +- `GH_EDITOR` - Editor for composing text diff --git a/instructions/markdown.instructions.md b/instructions/markdown.instructions.md new file mode 100644 index 0000000..430da58 --- /dev/null +++ b/instructions/markdown.instructions.md @@ -0,0 +1,123 @@ +--- +applyTo: '**/*.md' +description: 'Markdown formatting standards' +--- + +# Markdown Style Guidelines + +Consistent Markdown formatting for documentation files. + +## Blank Lines + +- Use single blank lines between sections and elements +- Never use multiple consecutive blank lines +- Headings, lists, and code blocks must have a blank line above and below + +## Headings + +- Use ATX style (`#`) not setext (underlines) +- Use consistent heading levels (don't skip levels) +- Start with a single H1 (`#`) for the document title +- Use sentence case for headings +- Include a space after `#` characters +- No trailing punctuation (colons, periods, etc.) +- Avoid duplicate heading text within the same document + +## Lists + +- Use `-` for unordered lists +- Use sequential numbering for ordered lists (`1.`, `2.`, `3.`, etc.) +- Use 2 spaces for nested list indentation + +```markdown +Text before list. + +- First item +- Second item + - Nested item + - Another nested item +- Third item + +Text after list. +``` + +## Code Blocks + +- Use backticks (`` ` ``) not tildes (`~`) for code fences +- Always specify language for fenced code blocks +- Ensure closing triple backticks are on their own line +- No trailing whitespace after closing backticks +- Code inside fenced blocks should follow the conventions of the relevant language's instruction + file (e.g., PowerShell snippets follow `powershell.instructions.md`) + +```javascript +// JavaScript code here +``` + +```python +# Python code here +``` + +```bash +# Shell code here +``` + +## Inline Formatting + +- Use `**bold**` for strong emphasis +- Use `*italic*` for light emphasis +- Use backticks for `code`, `filenames`, and `commands` +- Use backticks for keyboard shortcuts like `Ctrl+C` +- No spaces inside emphasis markers (`**text**` not `** text **`) +- No spaces inside backticks (`` `code` `` not `` ` code ` ``) +- Don't use bold/emphasis as a substitute for headings + +## Links + +- Use descriptive link text (not "click here") +- Use reference-style links for long URLs +- Use reference-style links when the same URL appears multiple times +- Links must have valid destinations (no empty hrefs) + +```markdown +See the [official documentation][docs] for more details. +The [documentation][docs] covers advanced topics. + +[docs]: https://example.com/documentation +``` + +## Images + +- Always include alt text for accessibility +- Use descriptive alt text that conveys the image content + +```markdown +![Diagram showing data flow between components](./images/data-flow.png) +``` + +## Line Length + +- Wrap prose at 80-100 characters when practical +- Don't wrap tables - maintain table formatting +- Don't wrap URLs or code blocks + +## File Structure + +- End all files with exactly one newline character +- No trailing whitespace on any lines +- Use spaces, not hard tabs +- Use UTF-8 encoding +- Avoid inline HTML when markdown alternatives exist + +## Tables + +- Align columns for readability in source +- Use header row separators +- Keep tables simple when possible + +```markdown +| Column 1 | Column 2 | Column 3 | +|----------|----------|----------| +| Value 1 | Value 2 | Value 3 | +| Value 4 | Value 5 | Value 6 | +``` diff --git a/instructions/powershell.instructions.md b/instructions/powershell.instructions.md new file mode 100644 index 0000000..8493861 --- /dev/null +++ b/instructions/powershell.instructions.md @@ -0,0 +1,515 @@ +--- +applyTo: '**/*.ps1,**/*.psm1,**/*.psd1' +description: 'PowerShell coding standards and best practices' +--- + +# PowerShell Style Guidelines + +Style rules for PowerShell code based on Microsoft guidelines and community standards. + +## Common Mistakes to Avoid + +**IMPORTANT**: These are frequent violations that MUST be avoided: + +1. **Plural nouns in function names** - ALWAYS use singular nouns regardless of how many items the + function returns. Use `Get-User` not `Get-Users`, `Get-Item` not `Get-Items`. + +## Function Structure + +1. Always start functions with `[CmdletBinding()]` attribute +2. Always include explicit `param()` block +3. Use `process {}` block when accepting pipeline input +4. For system-modifying cmdlets, use `[CmdletBinding(SupportsShouldProcess)]` +5. Document output types with `[OutputType([TypeName])]` attribute +6. Include comment-based help for all functions +7. Do not define nested functions inside other functions; define helper functions at module or + script scope + +```powershell +# Bad - nested function +function Get-Data { + [CmdletBinding()] + param() + + function Format-Result { + param($Value) + # Helper logic + } + + $result = Get-RawData + Format-Result -Value $result +} + +# Good - separate functions at module/script scope +function Format-Result { + [CmdletBinding()] + [OutputType([psobject])] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [psobject] + $Value + ) + # Helper logic +} + + +function Get-Data { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $Name + ) + + # Implementation +} + +# Function with pipeline input +function Get-PipelineInput { + [CmdletBinding()] + [OutputType([PSCustomObject])] + param( + [Parameter(ValueFromPipeline)] + [ValidateNotNull()] + [string] + $InputData + ) + + process { + # Process each pipeline item + } +} +``` + +## Type Accelerators + +Prefer type accelerators over full .NET type names: + +- `[string]`, `[int]`, `[bool]`, `[array]`, `[hashtable]` +- `[PSCustomObject]`, `[PSCredential]`, `[datetime]`, `[regex]` + +```powershell +# Good - type accelerators in parameter declarations +function Get-Setting { + [CmdletBinding()] + [OutputType([PSCustomObject])] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [hashtable] + $Configuration + ) +} + +# Avoid - full .NET type names +function Get-Setting { + [CmdletBinding()] + [OutputType([System.Management.Automation.PSCustomObject])] + param( + [Parameter(Mandatory)] + [System.Collections.Hashtable] + $Configuration + ) +} +``` + +## Naming Conventions + +1. Use approved PowerShell verbs only (verify with `Get-Verb`) +2. Use singular nouns for function names (`Get-Item` not `Get-Items`) +3. Use PascalCase for function names and parameters +4. Use camelCase for local variables (`$userName`, `$itemCount`) +5. Use descriptive variable names that indicate purpose +6. Use full cmdlet names, never aliases (`Get-Process` not `gps`) + +```powershell +# Good - descriptive variable names +$backupPath = 'C:\Backups' +$backupFiles = Get-ChildItem -Path $backupPath -Filter '*.bak' +$activeUsers = Get-ADUser -Filter { Enabled -eq $true } + +# Bad - generic variable names +$files = Get-ChildItem -Path $backupPath -Filter '*.bak' +$users = Get-ADUser -Filter { Enabled -eq $true } +``` + +### Path vs Directory Naming + +Use the appropriate suffix to indicate what the variable holds: + +- Use `Path` for any path string (file or folder) +- Reserve `Directory` for directory objects (e.g., `[System.IO.DirectoryInfo]`) or bare folder names + +```powershell +# Good - Path suffix for path strings +$configurationPath = Join-Path -Path $PSScriptRoot -ChildPath 'config.json' +$outputPath = Join-Path -Path $PSScriptRoot -ChildPath 'results' +$backupPath = 'C:\Backups' + +# Good - Directory suffix for a directory object +$logDirectory = [System.IO.DirectoryInfo]::new('C:\Logs') + +# Bad - Directory suffix on a path string +$outputDirectory = 'C:\App\results' +``` + +## Parameters + +1. Use full parameter names in scripts and functions +2. Always use quotes around string parameter values +3. Include validation on every parameter +4. Place each component on its own line + +```powershell +# Good - string parameter values are quoted +Get-Process -Name 'powershell' +Get-ChildItem -Path 'C:\Program Files' -Filter '*.txt' + +# Bad - bare string parameter values +Get-Process -Name powershell +Get-ChildItem -Path C:\Program Files -Filter *.txt +``` + +```powershell +function Get-UserData { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $UserName, + + [Parameter()] + [ValidateRange(1, 100)] + [int] + $MaxResults = 10, + + [Parameter(ValueFromPipeline)] + [ValidateNotNull()] + [string[]] + $ComputerName + ) +} +``` + +## Formatting + +1. Opening brace `{` at end of line, closing brace `}` on new line +2. Use 4 spaces per indentation level +3. Maximum line length: 115 characters +4. Use splatting for long parameter lists +5. Two blank lines before function definitions +6. One blank line at end of file + +```powershell +function Test-Code { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateRange(1, 100)] + [int] + $Value + ) + + if ($Value -gt 10) { + Write-Output 'Greater' + } + elseif ($Value -eq 10) { + Write-Output 'Equal' + } + else { + Write-Output 'Lesser' + } +} + +# Good - splatting for readability +$invokeRestMethodParameters = @{ + Uri = 'https://api.example.com/endpoint' + Method = 'Post' + Headers = $headers + Body = $body +} +Invoke-RestMethod @invokeRestMethodParameters +``` + +## Line Continuation + +1. Do not use backtick (`` ` ``) line continuation +2. Do not use semicolons (`;`) to chain multiple statements on one line +3. Prefer splatting (`@copyItemParameters`) for long parameter lists +4. Use natural continuation inside `()`, `@{}`, or `@()` when grouping expressions or collections +5. Place each hashtable element on its own line in multi-line hashtables +6. Pipelines continue without backticks when the line ends with `|` + +```powershell +# Good - splatting for long parameter lists +$copyItemParameters = @{ + Path = $sourcePath + Destination = $destinationPath + Recurse = $true + Force = $true +} +Copy-Item @copyItemParameters + +# Good - pipeline continues across lines +Get-ChildItem -Path $sourceDirectory -Recurse | + Where-Object { $_.Length -gt 1MB } | + Sort-Object -Property 'Length' -Descending + +# Good - natural continuation inside parentheses +$summaryMessage = ( + "Processed $successCount of $totalCount records. " + + "Skipped $skipCount records. " + + "Encountered $errorCount errors." +) + +# Good - for-loop semicolons are syntactic, not statement chaining +for ($i = 0; $i -lt 10; $i++) { + Write-Output -InputObject $i +} + +# Good - hashtable with each element on its own line +$webRequestOptions = @{ + Name = 'Value' + Size = 100 +} + +# Bad - backtick line continuation +Copy-Item -Path $sourcePath ` + -Destination $destinationPath ` + -Recurse ` + -Force + +# Bad - semicolons chaining statements +Import-Module -Name 'PSReadLine'; Set-PSReadLineOption -EditMode 'Emacs' + +# Bad - hashtable elements chained with semicolons on one line +$webRequestOptions = @{ Name = 'Value'; Size = 100 } +``` + +## Paths and File System + +1. Use `$PSScriptRoot` for script-relative paths +2. Use `$Env:UserProfile` or `$HOME` instead of `~` +3. Use `Join-Path` to construct paths + +```powershell +# Good +$configurationPath = Join-Path -Path $PSScriptRoot -ChildPath 'config.json' +$documentsPath = Join-Path -Path $Env:UserProfile -ChildPath 'Documents' + +# Bad +$configurationPath = '.\config.json' +$documentsPath = '~\Documents' +``` + +## Error Handling + +1. Use `-ErrorAction 'Stop'` for cmdlets within try/catch +2. Immediately copy `$_` in catch blocks before other commands + +```powershell +$filePath = 'C:\Data\settings.json' +try { + Get-Item -Path $filePath -ErrorAction 'Stop' +} +catch { + $errorRecord = $_ # Capture immediately + Write-Error "Failed: $($errorRecord.Exception.Message)" +} +``` + +## Credential Handling + +1. Use `[PSCredential]` for credential parameters, never `[string]` for passwords +2. Make credentials optional when the function can run without them +3. Use `[System.Management.Automation.Credential()]` attribute for flexibility + +```powershell +function Connect-Service { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] + $Server, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + # Check if credentials were provided + if ($Credential -eq [System.Management.Automation.PSCredential]::Empty) { + # Use current user context + } + else { + # Use provided credentials + } +} +``` + +## Output + +1. Write objects to pipeline immediately, don't batch into arrays +2. Use `Write-Verbose` for detailed operation information +3. Use `Write-Warning` for potential issues + +```powershell +# Good - immediate output +foreach ($item in $collection) { + $result = Format-Item -InputObject $item + $result # Output immediately +} + +# Bad - batching +$results = @() +foreach ($item in $collection) { + $results += Format-Item -InputObject $item +} +$results +``` + +## Documentation + +All functions must include comment-based help: + +```powershell +function Get-UserData { + <# + .SYNOPSIS + Brief one-line description. + + .DESCRIPTION + Detailed description of behavior. + + .PARAMETER UserName + Description of the parameter. + + .EXAMPLE + Get-UserData -UserName 'jsmith' + + Retrieves data for user jsmith. + + .OUTPUTS + System.Management.Automation.PSCustomObject + #> + [CmdletBinding()] + [OutputType([PSCustomObject])] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $UserName + ) + + # Implementation +} +``` + +## Quotes + +1. Use single quotes for string literals +2. Use double quotes only when variable expansion is needed +3. Quote hashtable keys only when necessary (hyphens, spaces) + +```powershell +# Good +$headers = @{ + Authorization = "Bearer $token" # Needs expansion + 'User-Agent' = 'PowerShell' # Key has hyphen +} + +$branchName = "feature/issue-$issueNumber" +$title = 'Static string' +``` + +## Spacing + +1. Spaces around all operators: `$x = 1 + 2` +2. Spaces around comparison operators: `$value -eq 10` +3. Space after commas and semicolons +4. No trailing spaces + +## Build Systems + +When a repository uses a build system (psake, Invoke-Build, etc.), use the build system's tasks for +operations like testing, building, publishing, and deployment rather than running commands directly +or creating separate scripts. Check for common build files: + +- `psakefile.ps1` or `psake.ps1` (psake) +- `*.build.ps1` (Invoke-Build) +- `build.ps1` (general build script) + +```powershell +# Good - use the build system +Invoke-psake -taskList Test +Invoke-Build -Task Test + +# Avoid - bypassing the build system +Invoke-Pester -Path .\tests\ +``` + +## Static Analysis + +PSScriptAnalyzer warnings indicate real issues. Fix the underlying problem rather than suppressing warnings. + +### Warnings to Always Fix + +These warnings represent naming and style violations that should be corrected: + +- **PSUseSingularNouns** - Rename function to use singular noun (`Get-Item` not `Get-Items`) +- **PSUseApprovedVerbs** - Use an approved verb from `Get-Verb` +- **PSAvoidUsingCmdletAliases** - Replace alias with full cmdlet name +- **PSAvoidUsingWriteHost** - Use `Write-Output`, `Write-Verbose`, or `Write-Information` + +```powershell +# Bad - suppressing instead of fixing +function Get-Items { # PSUseSingularNouns warning + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] + [CmdletBinding()] + param() + # Returns multiple items +} + +# Good - fix the naming +function Get-Item { + [CmdletBinding()] + param() + # Returns zero, one, or more items (singular noun is correct regardless) +} +``` + +### Suppression Requirements + +When suppression is genuinely necessary (rare), include a justification: + +1. Use `SuppressMessageAttribute` with the `Justification` parameter +2. Explain why the warning cannot be resolved +3. Reference external constraints if applicable + +```powershell +# Acceptable - justified suppression for API compatibility +function Get-AWSItems { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseSingularNouns', + '', + Justification = 'Matches AWS SDK naming convention for consistency with existing tooling' + )] + [CmdletBinding()] + param() +} +``` + +### Never Suppress Without Justification + +Suppressions without justification are not acceptable: + +```powershell +# Never do this +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] +``` diff --git a/instructions/readme.instructions.md b/instructions/readme.instructions.md new file mode 100644 index 0000000..5b9ea49 --- /dev/null +++ b/instructions/readme.instructions.md @@ -0,0 +1,31 @@ +--- +applyTo: '**/README.md' +description: 'README maintenance guidelines' +--- + +# README Instructions + +## Purpose + +Guidelines and requirements for maintaining `README.md` files in repositories using these instructions. + +## Requirements + +- The `README.md` must always reflect the current repository structure, naming conventions, and + setup instructions. +- Whenever you make changes to files, instructions, or workflows, review and update the + `README.md` to ensure: + - All file and directory names are accurate and up to date + - All setup and usage instructions match the latest practices and workflows + - The repository structure diagram is correct + - Any new or deprecated features are documented +- Use clear, concise language and follow Markdown best practices (see `markdown.instructions.md`). +- Place repository-specific or maintainer-only notes in clearly marked sections. +- Reference the CHANGELOG.md file for a summary of recent changes. + +## Best Practices + +- When making changes to instructions, templates, or workflows, update the `README.md` as part of + your change process. +- Include a checklist item for README review in pull requests and release notes. +- Ensure consistency between the `README.md`, instruction files, and actual repository contents. diff --git a/instructions/releases.instructions.md b/instructions/releases.instructions.md new file mode 100644 index 0000000..17aa7c7 --- /dev/null +++ b/instructions/releases.instructions.md @@ -0,0 +1,85 @@ +--- +applyTo: '**/*' +description: 'Release management guidelines for AI agents' +--- + +# Release Instructions for AI Agents + +When creating releases, AI agents must follow these guidelines to ensure proper release +management and avoid formatting issues. + +These instructions are self-contained for release processes but reference +repository-specific.instructions.md for additional requirements. + +## Creating Releases + +Always use `--notes-file` instead of `--notes` when creating GitHub releases to avoid escaping +issues with special characters (backticks, backslashes, quotes, etc.): + +```powershell +# Create a temporary file with release notes +$releaseNotes = @" +## Added + +- Your changes here + +## Changed + +- Your changes here + +## Fixed + +- Your changes here +"@ + +$releaseNotes | Out-File -FilePath "release-notes.md" -Encoding utf8 +gh release create v0.x.y --title "v0.x.y - Release Title" --notes-file release-notes.md +Remove-Item "release-notes.md" +``` + +## Release Notes Format + +Release notes should follow the [Keep a Changelog](https://keepachangelog.com/) format: + +- Use standard sections: Added, Changed, Deprecated, Removed, Fixed, Security +- Write clear, user-focused descriptions +- Reference issue numbers or PRs where relevant +- Use present tense for changes ("Add feature" not "Added feature" in the section items) +- Keep descriptions concise but informative + +## Version Numbering + +Follow [Semantic Versioning](https://semver.org/): + +- **MAJOR** version (x.0.0): Breaking changes or incompatible API changes +- **MINOR** version (0.x.0): New features in a backward-compatible manner +- **PATCH** version (0.0.x): Backward-compatible bug fixes + +## Pre-Release Checklist + +Follow `git-workflow.instructions.md` for branching and PR workflow. The steps below are +release-specific: + +1. **Verify current release state**: Run `gh release list --limit 5` to check the most recent + releases. Compare the latest released version against the version in CHANGELOG.md. If they + match, the changelog version needs to be incremented. If the changelog is already ahead, use + that version. NEVER release a version that already exists. +2. **Check repository-specific instructions**: Review `repository-specific.instructions.md` for + any additional release requirements specific to this repository +3. **Update CHANGELOG.md**: Add new version section with all changes +4. **Update version numbers**: Bump version in relevant files as needed +5. **Update changelog links**: Add comparison link for the new version at the bottom of + CHANGELOG.md (e.g., `[0.2.0]: https://github.com/owner/repo/compare/v0.1.0...v0.2.0`) +6. **Run tests**: Ensure all tests pass +7. **Commit changes**: Commit all version updates +8. **Create PR and wait for merge**: Follow the PR workflow in `git-workflow.instructions.md` +9. **Create release**: After PR is merged, use `gh release create` with `--notes-file` + +## Post-Release + +After creating a release: + +1. Verify the release appears correctly on GitHub +2. Check that release notes display properly (no formatting issues) +3. Confirm download links work if applicable +4. Notify team members if this is a significant release diff --git a/instructions/repository-specific.instructions.md b/instructions/repository-specific.instructions.md new file mode 100644 index 0000000..f805c82 --- /dev/null +++ b/instructions/repository-specific.instructions.md @@ -0,0 +1,388 @@ +--- +applyTo: '**/*' +description: 'Repository-specific instructions for the PowerShellBuild module' +--- + +# Repository-Specific Instructions + +These instructions cover the parts of PowerShellBuild that an agent cannot infer from the +standard AIM modules (`powershell`, `git-workflow`, `testing`, etc.). Read those first; this +file only adds repo-specific concepts. + +## Repository Context + +**PowerShellBuild** is a PowerShell module that provides standardized build, test, and publish +tasks for other PowerShell module projects. It supports two task-runner frameworks: + +- **psake** (4.9.0+) +- **Invoke-Build** (5.8.1+) + +- Current version: **0.8.1** (see `PowerShellBuild/PowerShellBuild.psd1`) +- `PowerShellVersion` in the manifest is currently `'3.0'` — almost certainly wrong; under + review in the v1.0.0 roadmap (psake/PowerShellBuild#120) +- Cross-platform: Windows, Linux, macOS (CI matrix in `.github/workflows/test.yml`) +- The module is **psake/PowerShellBuild** on PSGallery and GitHub; maintained by the psake org + +## Repository Layout + +```text +PowerShellBuild/ +├── Build/Convert-PSAke.ps1 # Dev utility: converts psake tasks to Invoke-Build (not shipped) +├── PowerShellBuild/ # THE MODULE SOURCE (system under test) +│ ├── Public/ # 12 exported functions +│ ├── Private/ # Internal helpers +│ ├── en-US/Messages.psd1 # Localized strings (Import-LocalizedData) +│ ├── PowerShellBuild.psd1 # Manifest +│ ├── PowerShellBuild.psm1 # Dot-sources Public/ and Private/ +│ ├── ScriptAnalyzerSettings.psd1 +│ ├── build.properties.ps1 # $PSBPreference (canonical config hashtable) +│ ├── psakeFile.ps1 # Tasks consumers import +│ └── IB.tasks.ps1 # Invoke-Build entry (aliased as PowerShellBuild.IB.Tasks) +├── tests/ # Pester 5+ tests +│ └── TestModule/ # Sample module exercised by the test suite +├── build.ps1 # Main build entry point for THIS repo +├── build.settings.ps1 # Build settings for THIS repo's own psake build +├── psakeFile.ps1 # psake tasks for building THIS repo (simpler than the inner one) +└── requirements.psd1 # PSDepend manifest +``` + +**The two `psakeFile.ps1` files serve different purposes:** + +- Root `psakeFile.ps1` → builds *this* repo +- `PowerShellBuild/psakeFile.ps1` → what consumers import to build *their* repo + +## Key Concepts + +### `$PSBPreference` — the central configuration object + +All build behavior is controlled through a single ordered hashtable named `$PSBPreference`, +defined in `PowerShellBuild/build.properties.ps1`. The variable name is fixed — never rename +it or recreate it under a different name. + +It is set as a **read-only script-scoped variable** when `psakeFile.ps1` is loaded. To modify +it, set values *before* loading the task file, or use `Set-Variable -Force`. + +Sections: + +| Section | Purpose | +| -------------- | ------------------------------------------------------------------------------------------------------------------ | +| `General` | ProjectRoot, SrcRootDir, ModuleName, ModuleVersion, ModuleManifestPath | +| `Build` | OutDir, ModuleOutDir, CompileModule, CompileDirectories, CopyDirectories, Exclude | +| `Test` | Enabled, RootDir, OutputFile/Format, ScriptAnalysis, CodeCoverage, ImportModule, etc. | +| `Help` | UpdatableHelpOutDir, DefaultLocale, ConvertReadMeToAboutHelp | +| `Docs` | RootDir, Overwrite, AlphabeticParamsOrder, ExcludeDontShow, UseFullTypeName | +| `Publish` | PSRepository, PSRepositoryApiKey, PSRepositoryCredential | +| `Sign` | Enabled, CertificateSource, CertStoreLocation, Thumbprint, EnvVar/PfxFile sources, TimestampServer, HashAlgorithm, FilesToSign, Catalog | +| `Sign.Catalog` | Enabled, Version, FileName | + +### Module compilation modes + +`$PSBPreference.Build.CompileModule` controls how the module is staged to the output directory: + +- `$false` (default) — files copied as-is, preserving the `Public/`/`Private/` structure +- `$true` — all `.ps1` files from `CompileDirectories` (default: `Enum`, `Classes`, `Private`, + `Public`) are concatenated into a single `.psm1`. Optional `CompileHeader`, `CompileFooter`, + `CompileScriptHeader`, and `CompileScriptFooter` strings can be injected. + +### Task dependency variables + +Task dependencies in `PowerShellBuild/psakeFile.ps1` are defined via variables checked with +`if ($null -eq ...)`. This lets consumers override dependencies *before* importing the tasks +file: + +```powershell +# Example: insert a custom task before Pester runs +$PSBPesterDependency = @('Build', 'MyCustomTask') +``` + +Variables (pattern: `$PSB{TaskName}Dependency`): + +| Variable | Default | +| ------------------------------------- | -------------------------------- | +| `$PSBCleanDependency` | `@('Init')` | +| `$PSBStageFilesDependency` | `@('Clean')` | +| `$PSBBuildDependency` | `@('StageFiles', 'BuildHelp')` | +| `$PSBAnalyzeDependency` | `@('Build')` | +| `$PSBPesterDependency` | `@('Build')` | +| `$PSBTestDependency` | `@('Pester', 'Analyze')` | +| `$PSBBuildHelpDependency` | `@('GenerateMarkdown', 'GenerateMAML')` | +| `$PSBGenerateMarkdownDependency` | `@('StageFiles')` | +| `$PSBGenerateMAMLDependency` | `@('GenerateMarkdown')` | +| `$PSBGenerateUpdatableHelpDependency` | `@('BuildHelp')` | +| `$PSBPublishDependency` | `@('Test')` | +| `$PSBSignModuleDependency` | `@('Build')` | +| `$PSBBuildCatalogDependency` | `@('SignModule')` | +| `$PSBSignCatalogDependency` | `@('BuildCatalog')` | +| `$PSBSignDependency` | `@('SignCatalog')` | + +## Public API (Exported Functions) + +12 exported functions in `PowerShellBuild/Public/` (all follow the `Verb-PSBuildNoun` naming +pattern — keep new public functions consistent with this): + +| Function | Description | +| ------------------------------ | ---------------------------------------------------------------------- | +| `Initialize-PSBuild` | Sets up BuildHelpers env vars, displays build info | +| `Build-PSBuildModule` | Copies/compiles module source to output directory | +| `Clear-PSBuildOutputFolder` | Safely removes the build output directory | +| `Build-PSBuildMarkdown` | Generates PlatyPS Markdown docs from module help | +| `Build-PSBuildMAMLHelp` | Converts PlatyPS Markdown to MAML XML help files | +| `Build-PSBuildUpdatableHelp` | Creates a `.cab` file for updatable help | +| `Test-PSBuildPester` | Runs Pester tests with configurable output and coverage | +| `Test-PSBuildScriptAnalysis` | Runs PSScriptAnalyzer with a configurable severity threshold | +| `Publish-PSBuildModule` | Publishes the built module to a PowerShell repository | +| `Get-PSBuildCertificate` | Resolves an Authenticode signing certificate | +| `Invoke-PSBuildModuleSigning` | Signs module files with an Authenticode certificate | +| `New-PSBuildFileCatalog` | Generates a `.cat` file catalog for the module | + +Private helper: `Remove-ExcludedItem` — filters file system items by regex patterns during builds. + +### Invoke-Build alias + +The module exports an alias `PowerShellBuild.IB.Tasks` that points to `IB.tasks.ps1`, enabling +the Invoke-Build dot-source pattern: + +```powershell +# In a consumer's .build.ps1 for Invoke-Build +Import-Module PowerShellBuild +. PowerShellBuild.IB.Tasks +``` + +## Build Workflows + +### Building this repo + +The repo uses its own psake build. Main entry point is `./build.ps1`. **Run with PowerShell 7+ +(`pwsh`).** + +```powershell +# First time in a fresh env — installs deps via PSDepend +./build.ps1 -Bootstrap + +# Specific tasks +./build.ps1 -Task Build +./build.ps1 -Task Test +./build.ps1 -Task Analyze +./build.ps1 -Task Pester + +# List available tasks +./build.ps1 -Help + +# Publish to PSGallery (requires API-key credential) +./build.ps1 -Task Publish -PSGalleryApiKey $cred +``` + +### Repo-level tasks (root `psakeFile.ps1`) + +| Task | Depends On | Description | +| --------- | ---------------------- | ------------------------------------------ | +| `default` | Test | Default task | +| `Init` | — | Initialize build env (shows `BH*` env vars)| +| `Clean` | Init | Remove output directory | +| `Build` | Init, Clean | Copy module source to output | +| `Analyze` | Build | Run PSScriptAnalyzer | +| `Pester` | Build | Run Pester tests | +| `Test` | Init, Analyze, Pester | Run all tests | +| `Publish` | Test | Publish to PSGallery | + +### Module-level tasks (consumer-facing `PowerShellBuild/psakeFile.ps1`) + +These are the tasks consumer modules get when they import PowerShellBuild: + +| Task | Description | +| ----------------------- | -------------------------------------------- | +| `Init` | Initialize build env variables | +| `Clean` | Clear module output directory | +| `StageFiles` | Copy/compile source to output | +| `Build` | StageFiles + BuildHelp | +| `Analyze` | PSScriptAnalyzer | +| `Pester` | Pester tests | +| `Test` | Pester + Analyze | +| `GenerateMarkdown` | PlatyPS Markdown from help | +| `GenerateMAML` | MAML XML from Markdown | +| `BuildHelp` | GenerateMarkdown + GenerateMAML | +| `GenerateUpdatableHelp` | CAB file for updatable help | +| `Publish` | Publish to repository | +| `SignModule` | Authenticode-sign module files (`*.psd1`, `*.psm1`, `*.ps1`) | +| `BuildCatalog` | Create Windows catalog (`.cat`) for the built module | +| `SignCatalog` | Authenticode-sign the module catalog file | +| `Sign` | Meta task — runs the full signing chain | + +Tasks with prerequisite modules (`Analyze`, `Pester`, `GenerateMarkdown`, `GenerateMAML`, +`GenerateUpdatableHelp`) check that required modules are installed; they skip gracefully +with a warning if the module is missing. + +The signing tasks (`SignModule`, `BuildCatalog`, `SignCatalog`) have similar preconditions: +they skip when `$PSBPreference.Sign.Enabled` is `$false` (catalog tasks also require +`$PSBPreference.Sign.Catalog.Enabled = $true`) or when the required Windows-only cmdlets +(`Set-AuthenticodeSignature`, `New-FileCatalog`) are not available — so signing safely +no-ops on non-Windows. + +## Dependencies + +Defined in `requirements.psd1`, installed via **PSDepend** when `./build.ps1 -Bootstrap` runs: + +| Module | Version | +| ---------------- | -------- | +| BuildHelpers | 2.0.16 | +| Pester | ≥ 5.6.1 | +| psake | 4.9.0 | +| PSScriptAnalyzer | 1.24.0 | +| InvokeBuild | 5.8.1 | +| platyPS | 0.14.2 | + +## Testing + +Tests live in `tests/` and use **Pester 5+** syntax. + +- Always build the module before running Pester directly — running against source can produce + incorrect results. Prefer `./build.ps1 -Task Test` over a raw `Invoke-Pester` call. +- `tests/TestModule/` is a complete example module used to exercise PowerShellBuild's tasks. + It has its own `build.ps1`, `psakeFile.ps1`, `.build.ps1` (Invoke-Build), and Pester tests. + +| Test file | Tests | +| ---------------------- | ----------------------------------------------------------------------- | +| `build.tests.ps1` | Module compilation, file staging, exclusion, header/footer injection | +| `Help.tests.ps1` | Help documentation completeness | +| `IBTasks.tests.ps1` | Invoke-Build task definitions | +| `Manifest.tests.ps1` | Module manifest validity | +| `Meta.tests.ps1` | Script analysis, best practices across module source | + +## CI / CD (GitHub Actions) + +### Test workflow (`.github/workflows/test.yml`) + +- Triggers: push to default branch, pull requests, manual dispatch +- Matrix: `ubuntu-latest`, `windows-latest`, `macOS-latest` +- Command: `./build.ps1 -Task Test -Bootstrap` +- Supports a `DEBUG` runner flag for verbose output + +### Publish workflow (`.github/workflows/publish.yaml`) + +- Triggers: manual dispatch, GitHub release published +- Runs on: `ubuntu-latest` +- Reads `PSGALLERY_API_KEY` secret, converts to `PSCredential`, runs + `./build.ps1 -Task Publish -PSGalleryApiKey $cred -Bootstrap` + +## Repo-Specific Conventions + +These supplement `powershell.instructions.md` and `git-workflow.instructions.md` — they +don't replace them. + +- **Function naming**: public functions follow `Verb-PSBuildNoun` (e.g., `Build-PSBuildModule`, + `Test-PSBuildPester`). Always use an approved verb. +- **Config variable**: always `$PSBPreference`. Never rename or recreate it. +- **Task dependency vars**: `$PSB{TaskName}Dependency` (e.g., `$PSBPesterDependency`). +- **Localization**: user-facing strings live in `PowerShellBuild/en-US/Messages.psd1` and load + via `Import-LocalizedData`. Add new strings there rather than hardcoding messages in + function bodies. Use UTF-8 with BOM (standard for PowerShell data files). +- **Script analysis**: PSScriptAnalyzer config is `PowerShellBuild/ScriptAnalyzerSettings.psd1`. + Default severity threshold for build failure is `Error`. Warnings are reported but don't + fail the build. +- **Spell-checker ignores**: inline comments — `# spell-checker:ignore MAML PSGALLERY`. + +## How Consumers Use This Module + +### With psake + +```powershell +# In consumer's psakeFile.ps1 +properties { + # These settings overwrite values supplied from the PowerShellBuild + # module and govern how those tasks are executed + $PSBPreference.Test.ScriptAnalysisEnabled = $false + $PSBPreference.Test.CodeCoverage.Enabled = $true +} + +task default -depends Build + +task Build -FromModule PowerShellBuild -Version '0.1.0' +``` + +### With Invoke-Build + +```powershell +# In consumer's .build.ps1 +Import-Module PowerShellBuild +. PowerShellBuild.IB.Tasks + +# Override configuration after dot-sourcing +$PSBPreference.Build.CompileModule = $false +``` + +## Common Development Tasks + +### Adding a new public function + +1. Create the file under `PowerShellBuild/Public/NewFunction.ps1` +2. Use the `Verb-PSBuildNoun` naming pattern +3. Add any user-facing strings to `PowerShellBuild/en-US/Messages.psd1` +4. Add the function name to `FunctionsToExport` in `PowerShellBuild.psd1` +5. No edit to `PowerShellBuild.psm1` needed — it dot-sources all files in `Public/` automatically + +### Adding a new build task + +1. Add the task to `PowerShellBuild/psakeFile.ps1` +2. Define a corresponding `$PSB{TaskName}Dependency` variable with an `if ($null -eq ...)` guard +3. If the task requires a new module, update `PowerShellBuild.psd1` and `requirements.psd1` + +### Updating module version + +1. Edit `ModuleVersion` in `PowerShellBuild/PowerShellBuild.psd1` +2. Add a `CHANGELOG.md` entry (see `releases.instructions.md` for format) + +## Environment Variables (set by BuildHelpers) + +`Initialize-PSBuild` calls `BuildHelpers\Set-BuildEnvironment`, which populates: + +| Variable | Value | +| ------------------------- | ---------------------------------------------------- | +| `$env:BHProjectPath` | Repository root directory | +| `$env:BHProjectName` | Module name (from directory structure) | +| `$env:BHPSModulePath` | Path to module source directory | +| `$env:BHPSModuleManifest` | Path to `.psd1` manifest | +| `$env:BHModulePath` | Same as `BHPSModulePath` | +| `$env:BHBuildSystem` | Detected CI system (e.g., `GitHubActions`, `Unknown`)| +| `$env:BHBranchName` | Current git branch | +| `$env:BHCommitMessage` | Latest git commit message | + +## Output Directory Structure + +After a successful build: + +```text +Output/ +└── PowerShellBuild/ + └── 0.8.0/ + ├── Public/ # (when CompileModule = $false) + ├── Private/ + ├── en-US/ + ├── PowerShellBuild.psd1 + ├── PowerShellBuild.psm1 + └── ScriptAnalyzerSettings.psd1 +``` + +When `CompileModule = $true`, all `.ps1` files are merged into the single `.psm1` and the +`Public/`/`Private/` directories are not copied to output. + +`Output/` is in `.gitignore` and excluded from VS Code search (`.vscode/settings.json`). + +## v1.0.0 Roadmap + +The v1.0.0 release is actively being planned in **psake/PowerShellBuild#120**. Locked-in +decisions include: PRs directly to `main`, `1.0.0-preview.N` prereleases after each phase, +hard cut + migration guide (no deprecation cycle), psake 5.x in scope. Phase-by-phase +breakdown lives in the tracking issue. + +Migration guide path (created in Phase 1): `docs/migration/v0.8-to-v1.0.md`. + +## Notes for AI Agents + +- **First-time setup**: always run `./build.ps1 -Bootstrap` in a fresh environment to install + dependencies via PSDepend. +- **`$PSBPreference` is read-only at script scope** once `psakeFile.ps1` is loaded. To modify + it, set values before loading the task file, or use `Set-Variable -Force`. +- **Tests need the module built first** — running Pester directly against source can produce + incorrect results. Use `./build.ps1 -Task Test` rather than raw `Invoke-Pester` unless the + module is already built and imported. +- `Build/Convert-PSAke.ps1` is a developer convenience tool, not part of the published module. diff --git a/instructions/shorthand.instructions.md b/instructions/shorthand.instructions.md new file mode 100644 index 0000000..de68961 --- /dev/null +++ b/instructions/shorthand.instructions.md @@ -0,0 +1,66 @@ +--- +applyTo: '**/*' +description: 'Guidelines for avoiding shorthand and abbreviations in all code and documentation.' +--- + +# Shorthand Guidelines + +## Avoid Shorthand and Abbreviations + +To maximize clarity, maintainability, and consistency across all code and documentation, always +use full, descriptive words instead of shorthand or abbreviations. + +- **Do not use**: `Params`, `Props`, `Config`, `Info`, `Temp`, `Env`, `Obj`, `Val`, `Ref`, + `Err`, `Msg`, etc. +- **Do use**: `Parameters`, `Properties`, `Configuration`, `Information`, `Temporary`, + `Environment`, `Object`, `Value`, `Reference`, `Error`, `Message`, etc. + +### Rationale + +- Shorthand and abbreviations can be ambiguous and reduce code readability. +- Full words make intent clear for all contributors and AI agents. +- Consistent naming improves searchability and onboarding for new team members. + +### Examples + +| Avoid | Prefer | +| ------ | ---------------------------- | +| Params | Parameters | +| Props | Properties | +| Config | Configuration | +| Info | Information | +| Temp | Temporary | +| Env | Environment | +| Obj | Object | +| Val | Value | +| Ref | Reference | +| Err | Error | +| Msg | Message | +| Conn | Connection / Connections | +| Dir | Directory | +| Cmd | Command | +| Svc | Service | +| Cfg | Configuration | +| Tmp | Temporary | +| Usr | User | +| Grp | Group | +| Ctx | Context | +| Auth | Authentication / Authorize | +| Util | Utility / Utilities | +| Init | Initialize / Initialization | +| Req | Request / Requirement | +| Resp | Response | +| ObjRef | Object Reference | +| Num | Number | + +### Additional Guidance + +- Never use abbreviations in parameter, property, or variable names unless they are + industry-standard and unambiguous (e.g., `ID`, `URL`). +- Use the singular or plural form of a word as appropriate for the context (e.g., use + `Connection` for a single item, `Connections` for collections or lists). +- If a project already uses a specific abbreviation as a standard, document it clearly in the + relevant instruction file. +- If new abbreviations are introduced in the future, document them here and avoid their use + unless absolutely necessary and unambiguous. +- This rule applies to all code, documentation, commit messages, and user-facing text. diff --git a/instructions/testing.instructions.md b/instructions/testing.instructions.md new file mode 100644 index 0000000..4753984 --- /dev/null +++ b/instructions/testing.instructions.md @@ -0,0 +1,339 @@ +--- +applyTo: '**/*' +description: 'Test writing best practices and conventions' +--- + +# Testing Instructions + +Language-agnostic guidelines for writing effective tests. + +## Discovering Existing Test Tooling + +Before creating scripts for test-related tasks (running tests, gathering coverage, generating reports): + +1. **Check for build systems** - Look for `Makefile`, `build.ps1`, `package.json` scripts, `tox.ini`, + `pyproject.toml`, or similar build configuration files +2. **Search README and CI configs** - Existing commands are often documented or visible in CI workflows +3. **Ask the user** - If unsure whether tooling exists, ask before creating anything new + +**Never create new scripts when existing build tooling already handles the task.** + +## Test Structure + +### Arrange-Act-Assert (AAA) + +Structure each test in three clear sections: + +```javascript +// Arrange - Set up test data and preconditions +// Act - Execute the code being tested +// Assert - Verify the expected outcome +``` + +**Example:** + +```javascript +// Arrange +user = createTestUser(name: "Alice", role: "admin") + +// Act +result = user.hasPermission("delete") + +// Assert +expect(result).toBe(true) +``` + +### Given-When-Then (BDD Style) + +Alternative structure for behavior-focused tests: + +```javascript +// Given - Initial context +// When - Action occurs +// Then - Expected outcome +``` + +## Naming Conventions + +### Test Names Should Describe Behavior + +**Pattern:** `__` + +**Good examples:** + +```text +calculateTotal_withEmptyCart_returnsZero +userLogin_withInvalidPassword_throwsAuthError +emailValidator_withValidEmail_returnsTrue +``` + +**Avoid:** + +```text +test1 +testCalculate +itWorks +``` + +### Test File Naming + +Place test files alongside source files or in a dedicated test directory: + +```text +src/ + calculator.js + calculator.test.js # Adjacent to source + +tests/ + calculator.test.js # Or in test directory +``` + +Common extensions: + +- `.test.js`, `.test.ts` +- `.spec.js`, `.spec.ts` +- `_test.go` +- `Test.cs` +- `.Tests.ps1` + +## Test Types + +### Unit Tests + +- Test individual functions or methods in isolation +- Mock external dependencies +- Fast execution (milliseconds) +- High coverage of edge cases + +### Integration Tests + +- Test interaction between components +- May use real databases or services +- Slower than unit tests +- Focus on component boundaries + +### End-to-End Tests + +- Test complete user workflows +- Use real browser/UI automation +- Slowest to execute +- Cover critical user paths + +## Best Practices + +### One Assertion Per Concept + +Each test should verify one logical concept: + +**Good:** + +```text +test_addItem_increasesCartCount +test_addItem_updatesCartTotal +``` + +**Avoid:** + +```text +test_addItem_doesEverything // Tests multiple things +``` + +### Test Independence + +- Each test should run independently +- Don't rely on test execution order +- Clean up test data after each test +- Use fresh fixtures for each test + +### Avoid Test Interdependence + +**Bad:** + +```javascript +test1_createUser() // Creates user +test2_loginUser() // Assumes user exists from test1 +``` + +**Good:** + +```javascript +test_loginUser() { + user = createTestUser() // Each test creates its own data + // ... test logic +} +``` + +### Use Descriptive Assertions + +**Good:** + +```javascript +expect(user.isActive).toBe(true) +expect(result).toContain("success") +expect(list).toHaveLength(3) +``` + +**Avoid:** + +```javascript +expect(x).toBe(true) // What is x? +assert(result) // What should result be? +``` + +## Test Data + +### Use Meaningful Test Data + +**Good:** + +```javascript +email = "valid.user@example.com" +invalidEmail = "not-an-email" +``` + +**Avoid:** + +```javascript +email = "test" +x = "asdf" +``` + +### Use Factories or Builders + +Create helper functions for test data: + +```javascript +function createTestUser(overrides = {}) { + return { + id: generateId(), + name: "Test User", + email: "test@example.com", + role: "user", + ...overrides + } +} + +// Usage +adminUser = createTestUser({ role: "admin" }) +``` + +### Edge Cases to Consider + +- Empty inputs (null, undefined, empty string, empty array) +- Boundary values (0, -1, max int, min int) +- Invalid inputs (wrong type, malformed data) +- Large inputs (performance edge cases) +- Special characters and unicode +- Concurrent access (race conditions) + +## Mocking and Stubbing + +### When to Mock + +- External services (APIs, databases) +- Time-dependent operations +- Random number generation +- File system operations +- Network requests + +### When Not to Mock + +- Simple value objects +- Pure functions with no side effects +- The code you're actually testing + +### Mock Guidelines + +- Only mock what you need +- Verify mock interactions when behavior matters +- Reset mocks between tests +- Prefer dependency injection for easier mocking + +## Test Coverage + +### Focus on Critical Paths + +Prioritize testing: + +1. Business-critical functionality +2. Error handling and edge cases +3. Security-sensitive code +4. Complex algorithms + +### Coverage Goals + +- Aim for meaningful coverage, not 100% +- High coverage doesn't guarantee quality +- Focus on testing behavior, not implementation details + +## Bug Fix Testing + +### Test-First Bug Fixing + +When fixing a bug, always follow this workflow: + +1. **Write a failing test first** - Create at least one test that reproduces the bug +2. **Verify the test fails** - Confirm the test fails for the expected reason +3. **Fix the bug** - Implement the minimal fix to make the test pass +4. **Verify all tests pass** - Ensure both the new test and existing tests pass + +**Example workflow:** + +```text +# 1. Create test that exposes the bug +test_calculateDiscount_withZeroQuantity_returnsZero() + # This test fails because of the bug + +# 2. Run tests - confirm failure +> npm test +FAIL: calculateDiscount returns NaN instead of 0 + +# 3. Fix the bug in the source code + +# 4. Run tests - confirm fix +> npm test +PASS: All tests passing +``` + +### Why Test-First Matters + +- **Proves the bug exists** - The failing test documents the exact issue +- **Prevents regressions** - The test ensures the bug won't return +- **Validates the fix** - You know the fix works when the test passes +- **Documents behavior** - Future developers understand the expected behavior + +### Bug Test Naming + +Name bug-related tests to indicate the scenario being fixed: + +```text +calculateTotal_withNullItems_returnsZeroInsteadOfCrashing +parseDate_withLeapYear_handlesFebruary29Correctly +userAuth_withExpiredToken_returnsUnauthorizedNotServerError +``` + +## Running Tests + +### Before Committing + +- Run related tests locally +- Ensure all tests pass +- Add tests for new functionality +- Update tests for changed behavior + +### Continuous Integration + +- Tests should run on every PR +- Failed tests should block merging +- Keep test suite fast (parallelize when possible) + +## Anti-Patterns to Avoid + +| Anti-Pattern | Problem | Solution | +| ----------------------- | ------------------ | ----------------------------- | +| Testing implementation | Brittle tests | Test behavior/outcomes | +| Flaky tests | Unreliable CI | Fix timing/ordering issues | +| Slow tests | Developer friction | Optimize or parallelize | +| No assertions | False confidence | Always verify outcomes | +| Commented-out tests | Hidden failures | Delete or fix tests | +| Test data in production | Security risk | Use separate test environment | diff --git a/instructions/update.instructions.md b/instructions/update.instructions.md new file mode 100644 index 0000000..623ed3c --- /dev/null +++ b/instructions/update.instructions.md @@ -0,0 +1,193 @@ +--- +applyTo: '**/*' +description: 'Procedures for updating AI agent instructions from the centralized repository' +--- + +# Update Instructions for AI Agents + +These instructions are self-contained for update procedures but assume familiarity with Git. +For general workflow guidance, see agent-workflow.instructions.md. + +## Configuration Schema + +Repositories control AIM behavior through `aim.config.json` in the repository root: + +```json +{ + "version": "latest", + "modules": { + "include": ["agent-workflow", "powershell", "markdown"], + "exclude": [] + }, + "externalSources": { + "enabled": true, + "repositories": [ + { + "name": "awesome-copilot", + "url": "https://github.com/github/awesome-copilot", + "path": "instructions", + "description": "Community-contributed instructions from GitHub" + } + ] + } +} +``` + +**Configuration fields:** + +- `version` - Target AIM version: `"latest"` or specific version (e.g., `"0.8.0"`) +- `modules.include` - List of modules to include (without `.instructions.md` extension) +- `modules.exclude` - List of modules to exclude (takes precedence over include) +- `externalSources.enabled` - Enable fetching from external repositories +- `externalSources.repositories` - List of external instruction sources + +## Update Procedure + +When updating AI agent instructions in a repository that uses AIM, AI agents should: + +### 1. Read Configuration + +- Check if `aim.config.json` exists in the repository root +- If it exists, read all configuration fields +- If it doesn't exist, use defaults: version=latest, all modules, externalSources disabled + +### 2. Clone the Centralized Repository + +- Clone: `git clone https://github.com/tablackburn/ai-agent-instruction-modules.git` +- If targeting a specific version (not "latest"), checkout that tag: `git checkout v0.8.0` +- Use `AGENTS.template.md` from the cloned repository, NOT `AGENTS.md` +- The file `AGENTS.md` in the centralized repository is that repository's own implementation +- The file `AGENTS.template.md` is the template for downstream repositories + +### 3. Summarize Changes + +- Read the current version from the downstream repository's `AGENTS.md` header + (e.g., "Template Version: 0.7.0") +- Read `CHANGELOG.md` from the cloned upstream repository +- Extract all version sections between the current version and the target version +- Provide the user with a brief summary of what has changed, noting any breaking changes +- If the current version equals the target version, inform the user they are already up to date + +### 4. Determine Modules to Sync + +Based on `aim.config.json`: + +- If `modules.include` is specified, only sync those modules +- If `modules.exclude` is specified, exclude those from the sync +- Core modules (`agent-workflow`, `update`) should always be included unless explicitly excluded +- `repository-specific.instructions.md` is NEVER copied from upstream + +### 5. Sync Instruction Files + +For each instruction file in the upstream `instruction-templates/` folder: + +1. Check if the module should be synced based on configuration +2. Check if the file already exists in the downstream `instructions/` folder +3. **If the file exists, ask the user:** + - "File X already exists. Overwrite with upstream version? (yes/no/diff)" + - If "diff", show the differences between local and upstream versions + - Only overwrite if the user confirms +4. **If the file is new**, copy it without prompting + +### 6. Handle External Sources + +If `externalSources.enabled` is true and a needed language/framework instruction is not found in +AIM: + +1. Check each configured external repository in order +2. For awesome-copilot, look in the `instructions/` path for matching `.instructions.md` files +3. Download the instruction file and copy to the downstream `instructions/` folder +4. Inform the user which files were fetched from external sources + +**Example external fetch:** + +```text +Fetching python.instructions.md from github/awesome-copilot... +Fetching react.instructions.md from github/awesome-copilot... +``` + +### 7. Update AGENTS.md + +- Replace the HTML comment block at the top (the comment starting with `