Skip to content

Commit 7956ca6

Browse files
feat(instructions): add PowerShell coding standards, Pester testing conventions, and bash copyright headers (#836)
## Description This PR introduces comprehensive **PowerShell coding standards** and **Pester testing conventions** to the hve-core project and adds **copyright header requirements** to the existing bash instructions. It also standardizes attribution conventions across AI artifact documentation. ### PowerShell Coding Standards > PowerShell scripting lacked formal coding guidance, leading to inconsistent patterns across the codebase. Added *powershell.instructions.md* establishing complete scripting standards for `.ps1`, `.psm1`, and `.psd1` files. The instruction covers a 10-section script structure, module patterns, naming conventions, error handling, CI integration via `Write-CIAnnotation`, parameter validation, PSScriptAnalyzer compliance, and copyright headers. Two complete working examples — a script template and a module template — demonstrate the conventions in practice. ### Pester Testing Conventions > Pester test files lacked dedicated coding guidance, causing inconsistent test structure across `scripts/tests/`. Added *pester.instructions.md* (245 lines) establishing Pester 5.x testing conventions for `.Tests.ps1` files. The instruction covers test naming and location, file headers (`#Requires`, copyright), SUT imports via `$PSScriptRoot`-relative paths, `Describe`/`Context`/`It` block structure, mocking with `Mock`/`InModuleScope`, assertions, tag conventions, output handling, and a complete annotated example template. Separated from *powershell.instructions.md* to keep each file focused and avoid excessive length. ### Bash Copyright Headers Updated *bash.instructions.md* with a copyright header section matching the PowerShell format (2-line Microsoft/SPDX-MIT). Corrected the attribution from *microsoft/edge-ai* to *microsoft/hve-core*. ### Attribution Convention Updated *ai-artifacts-common.md* to standardize attribution: frontmatter `description` field applies to all artifacts, while the blockquote footer is retained only for skill files (*SKILL.md*). ### Collection and Plugin Metadata Registered the new PowerShell and Pester instructions across collection manifests and plugin outputs. Updated *coding-standards.collection.md*, *coding-standards.collection.yml*, and *hve-core-all.collection.yml* to include PowerShell and Pester in descriptions, tags, and item lists. Updated *marketplace.json* and plugin metadata to reflect the expanded language coverage. Added plugin symlinks for both *coding-standards* and *hve-core-all* plugins. ## Related Issue(s) Closes #308 Closes #316 ## Type of Change Select all that apply: **Code &amp; Documentation:** * [ ] Bug fix (non-breaking change fixing an issue) * [x] New feature (non-breaking change adding functionality) * [ ] Breaking change (fix or feature causing existing functionality to change) * [x] Documentation update **Infrastructure &amp; Configuration:** * [ ] GitHub Actions workflow * [ ] Linting configuration (markdown, PowerShell, etc.) * [ ] Security configuration * [ ] DevContainer configuration * [ ] Dependency update **AI Artifacts:** * [ ] Reviewed contribution with `prompt-builder` agent and addressed all feedback * [x] Copilot instructions (`.github/instructions/*.instructions.md`) * [ ] Copilot prompt (`.github/prompts/*.prompt.md`) * [ ] Copilot agent (`.github/agents/*.agent.md`) * [ ] Copilot skill (`.github/skills/*/SKILL.md`) > **Note for AI Artifact Contributors**: > > * **Agents**: Research, indexing/referencing other project (using standard VS Code GitHub Copilot/MCP tools), planning, and general implementation agents likely already exist. Review `.github/agents/` before creating new ones. > * **Skills**: Must include both bash and PowerShell scripts. See [Skills](../docs/contributing/skills.md). > * **Model Versions**: Only contributions targeting the **latest Anthropic and OpenAI models** will be accepted. Older model versions (e.g., GPT-3.5, Claude 3) will be rejected. > * See [Agents Not Accepted](../docs/contributing/custom-agents.md#agents-not-accepted) and [Model Version Requirements](../docs/contributing/ai-artifacts-common.md#model-version-requirements). **Other:** * [ ] Script/automation (`.ps1`, `.sh`, `.py`) * [ ] Other (please describe): ## Sample Prompts (for AI Artifact Contributions) **User Request:** "Write a PowerShell script that validates configuration files" or "Create a PowerShell module for log processing." The instructions activate automatically via the `applyTo` glob pattern (`**/*.ps1`, `**/*.psm1`, `**/*.psd1`) when editing PowerShell files. For test files, "Write Pester tests for the log processor module" activates *pester.instructions.md* via the `applyTo` pattern (`**/*.Tests.ps1`). **Execution Flow:** 1. User creates or edits a `.ps1`, `.psm1`, or `.psd1` file in the workspace. 2. Copilot attaches *powershell.instructions.md* based on the `applyTo` pattern match. 3. Generated code follows the 10-section script structure: copyright header, `#Requires` statements, `using` statements, comment-based help, `[CmdletBinding()]` declaration, parameter block, `begin`/`process`/`end` blocks, and helper functions. 4. Error handling uses `try`/`catch` with `Write-CIAnnotation -Level Error` for CI-aware output. 5. Module files follow the module pattern with proper manifest (`.psd1`) and script module (`.psm1`) conventions. 6. When editing `*.Tests.ps1` files, *pester.instructions.md* activates and guides Pester 5.x test structure, SUT imports, mocking, and assertions. **Output Artifacts:** Generated PowerShell scripts contain structured sections following the convention: ```powershell # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. #Requires -Version 7.0 <# .SYNOPSIS Brief description of the script purpose. .DESCRIPTION Detailed description of what the script does. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$InputPath ) ``` Generated Pester tests follow the convention: ```powershell # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. #Requires -Version 7.0 #Requires -Modules @{ ModuleName = 'Pester'; RequiredVersion = '5.7.1' } Describe 'Function-Name' { BeforeAll { . "$PSScriptRoot/../../src/Function-Name.ps1" } Context 'when given valid input' { It 'returns expected output' { # Arrange, Act, Assert } } } ``` **Success Indicators:** - Generated PowerShell code passes `PSScriptAnalyzer` validation (`npm run lint:ps`). - Scripts include the 2-line Microsoft/SPDX-MIT copyright header. - Parameter names use PascalCase; variables use camelCase. - Error handling follows the `try`/`catch` pattern with `Write-CIAnnotation` for CI output. - Pester tests follow `Describe`/`Context`/`It` hierarchy with `BeforeAll` SUT imports. - Test files pass `npm run test:ps` execution. ## Testing Automated validation performed: - Markdown linting (`npm run lint:md`): **Passed** (287 files, 0 errors) - Spell checking (`npm run spell-check`): **Passed** (added `LASTEXITCODE` and `scriptblock` to cspell dictionary) - Frontmatter validation (`npm run lint:frontmatter`): **Passed** (271 files, 0 errors, 0 warnings) - Skill structure validation (`npm run validate:skills`): **Passed** (2 skills, 0 errors) - Link validation (`npm run lint:md-links`): **Passed** (12 pre-existing broken links in 7 unrelated files; 0 issues in PR files) - PowerShell analysis (`npm run lint:ps`): **Passed** (all scripts clean) - Plugin freshness (`npm run plugin:generate`): **Passed** (11 plugins, 147 instructions, 330 symlink index entries) Security analysis: No sensitive data exposure, dependency vulnerabilities, or privilege escalation concerns identified. Changes strengthen code quality and licensing compliance. Diff-based assessment: All changed files verified against commit history. No unintended modifications detected. Attribution correction (*edge-ai* → *hve-core*) confirmed consistent across bash instructions and plugin READMEs. Manual testing was not performed. ## Checklist ### Required Checks * [x] Documentation is updated (if applicable) * [x] Files follow existing naming conventions * [x] Changes are backwards compatible (if applicable) (N/A — new instruction files with no removal of existing surfaces) * [ ] Tests added for new functionality (if applicable) (N/A — instruction files do not require tests) ### AI Artifact Contributions * [ ] Used `/prompt-analyze` to review contribution * [ ] Addressed all feedback from `prompt-builder` review * [ ] Verified contribution follows common standards and type-specific requirements ### Required Automated Checks The following validation commands must pass before merging: * [x] Markdown linting: `npm run lint:md` * [x] Spell checking: `npm run spell-check` * [x] Frontmatter validation: `npm run lint:frontmatter` * [x] Skill structure validation: `npm run validate:skills` * [x] Link validation: `npm run lint:md-links` * [x] PowerShell analysis: `npm run lint:ps` * [x] Plugin freshness: `npm run plugin:generate` ## Security Considerations * [x] This PR does not contain any sensitive or NDA information * [x] Any new dependencies have been reviewed for security issues (N/A — no new dependencies) * [x] Security-related scripts follow the principle of least privilege (N/A — no security scripts modified) ## GHCP Artifact Maturity | File | Type | Maturity | Notes | |---|---|---|---| | *powershell.instructions.md* | Instructions | ✅ stable | All builds | | *pester.instructions.md* | Instructions | ✅ stable | All builds | | *bash.instructions.md* | Instructions | ✅ stable | All builds | ## Additional Notes - Plugin symlinks for the PowerShell and Pester instructions were added to both *coding-standards* and *hve-core-all* plugin directories. - The attribution convention change in *ai-artifacts-common.md* narrows blockquote footers to skill files only, simplifying the requirement for instructions, prompts, and agents. - *pester.instructions.md* was separated from *powershell.instructions.md* to keep each file focused and under 250 lines, following the convention of one concern per instruction file.
1 parent 551fddc commit 7956ca6

File tree

16 files changed

+781
-46
lines changed

16 files changed

+781
-46
lines changed

.cspell/general-technical.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,3 +1568,5 @@ streamlit
15681568
Streamlit
15691569
Overreliance
15701570
Contestability
1571+
LASTEXITCODE
1572+
scriptblock

.github/instructions/coding-standards/bash/bash.instructions.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
applyTo: '**/*.sh'
3-
description: 'Instructions for bash script implementation - Brought to you by microsoft/edge-ai'
3+
description: 'Instructions for bash script implementation - Brought to you by microsoft/hve-core'
44
---
55

66
# Bash Script Instructions
@@ -55,6 +55,32 @@ Encapsulate script logic in a `main()` function called at the end. This pattern:
5555
* Supports sourcing scripts for testing
5656
* Provides clear entry point
5757

58+
## Copyright Headers
59+
60+
Every `.sh` file requires a copyright header immediately after the shebang line.
61+
62+
Two required lines:
63+
64+
* `# Copyright (c) Microsoft Corporation.`
65+
* `# SPDX-License-Identifier: MIT`
66+
67+
Placement: after `#!/usr/bin/env bash`, before any other content.
68+
69+
CI validates copyright headers via `npm run validate:copyright` using `scripts/linting/Test-CopyrightHeaders.ps1`.
70+
71+
<!-- <example-copyright-header> -->
72+
```bash
73+
#!/usr/bin/env bash
74+
# Copyright (c) Microsoft Corporation.
75+
# SPDX-License-Identifier: MIT
76+
#
77+
# script-name.sh
78+
# Brief description of script purpose
79+
80+
set -euo pipefail
81+
```
82+
<!-- </example-copyright-header> -->
83+
5884
## Formatting and Style
5985

6086
### Indentation and Line Length
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
---
2+
description: "Instructions for Pester testing conventions - Brought to you by microsoft/hve-core"
3+
applyTo: '**/*.Tests.ps1'
4+
---
5+
6+
# Pester Testing Instructions
7+
8+
Pester 5.x is the testing framework for all PowerShell code. Tests run exclusively through `npm run test:ps`. Never invoke Pester or test scripts directly.
9+
10+
## Test File Naming
11+
12+
Test files use a `.Tests.ps1` suffix matching the production file name:
13+
14+
| Production file | Test file |
15+
|------------------------------|------------------------------------|
16+
| `Test-DependencyPinning.ps1` | `Test-DependencyPinning.Tests.ps1` |
17+
| `SecurityHelpers.psm1` | `SecurityHelpers.Tests.ps1` |
18+
| `SecurityClasses.psm1` | `SecurityClasses.Tests.ps1` |
19+
20+
## Test File Location
21+
22+
**Mirror directory pattern**: Test files in `scripts/tests/` mirror the production `scripts/` layout. Each production subdirectory has a corresponding test subdirectory:
23+
24+
| Production directory | Test directory |
25+
|------------------------|------------------------------|
26+
| `scripts/collections/` | `scripts/tests/collections/` |
27+
| `scripts/linting/` | `scripts/tests/linting/` |
28+
| `scripts/security/` | `scripts/tests/security/` |
29+
| `scripts/lib/` | `scripts/tests/lib/` |
30+
31+
**Co-located skill tests**: Skills place tests inside the skill directory rather than the mirror tree:
32+
33+
```text
34+
.github/skills/shared/pr-reference/
35+
├── scripts/
36+
│ ├── generate.ps1
37+
│ └── shared.psm1
38+
└── tests/
39+
├── generate.Tests.ps1
40+
└── shared.Tests.ps1
41+
```
42+
43+
## Test File Header
44+
45+
Test files place `#Requires -Modules Pester` before the copyright header. This ordering differs from production scripts:
46+
47+
```powershell
48+
#Requires -Modules Pester
49+
# Copyright (c) Microsoft Corporation.
50+
# SPDX-License-Identifier: MIT
51+
```
52+
53+
## SUT Import Patterns
54+
55+
Import the system under test in a file-level `BeforeAll` block. Use the pattern matching the production file type:
56+
57+
**Dot-source for scripts** (`.ps1`):
58+
59+
```powershell
60+
BeforeAll {
61+
. (Join-Path $PSScriptRoot '../../security/Test-DependencyPinning.ps1')
62+
}
63+
```
64+
65+
**Import-Module for modules** (`.psm1`):
66+
67+
```powershell
68+
BeforeAll {
69+
Import-Module (Join-Path $PSScriptRoot '../../security/Modules/SecurityHelpers.psm1') -Force
70+
}
71+
```
72+
73+
**using module for class modules** (parse-time type resolution):
74+
75+
```powershell
76+
using module ..\..\security\Modules\SecurityClasses.psm1
77+
```
78+
79+
The `using module` statement appears at the top of the file outside any block because PowerShell processes it at parse time.
80+
81+
## BeforeAll Setup
82+
83+
File-level `BeforeAll` initializes the test environment. Common activities include SUT import, mock module import, fixture path resolution, and output suppression:
84+
85+
```powershell
86+
BeforeAll {
87+
. (Join-Path $PSScriptRoot '../../security/Test-DependencyPinning.ps1')
88+
Import-Module (Join-Path $PSScriptRoot '../../security/Modules/SecurityHelpers.psm1') -Force
89+
Import-Module (Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1') -Force
90+
$script:FixtureRoot = Join-Path $PSScriptRoot '../Fixtures/Security'
91+
Mock Write-Host {}
92+
Mock Write-CIAnnotation {} -ModuleName SecurityHelpers
93+
}
94+
```
95+
96+
## Describe, Context, and It Blocks
97+
98+
All `Describe` blocks require `-Tag 'Unit'`. The pester configuration excludes `Integration` and `Slow` tags by default:
99+
100+
```powershell
101+
Describe 'FunctionName' -Tag 'Unit' {
102+
Context 'when input is valid' {
103+
It 'Returns expected output' {
104+
Get-Something -Path 'test' | Should -Be 'result'
105+
}
106+
}
107+
}
108+
```
109+
110+
`Context` groups related scenarios. Each `It` tests a single behavior with a descriptive sentence name.
111+
112+
## Data-Driven Tests
113+
114+
Use `-ForEach` on `It` blocks for parameterized testing:
115+
116+
```powershell
117+
It 'Accepts valid type <Value>' -ForEach @(
118+
@{ Value = 'Unpinned' }
119+
@{ Value = 'Stale' }
120+
@{ Value = 'VersionMismatch' }
121+
) {
122+
$v = [DependencyViolation]::new()
123+
$v.ViolationType = $Value
124+
$v.ViolationType | Should -Be $Value
125+
}
126+
```
127+
128+
## Mock Patterns
129+
130+
**Output suppression**: Empty scriptblock mocks prevent console noise:
131+
132+
```powershell
133+
Mock Write-Host {}
134+
```
135+
136+
**Module-scoped mocks**: `-ModuleName` injects mocks into modules under test:
137+
138+
```powershell
139+
Mock Write-CIAnnotation {} -ModuleName SecurityHelpers
140+
```
141+
142+
**Parameter-filtered mocks**: `-ParameterFilter` targets specific invocations:
143+
144+
```powershell
145+
Mock git {
146+
$global:LASTEXITCODE = 0
147+
return 'abc123'
148+
} -ModuleName LintingHelpers -ParameterFilter {
149+
$args[0] -eq 'merge-base'
150+
}
151+
```
152+
153+
## Mock Verification
154+
155+
Use `Should -Invoke` to verify mock calls:
156+
157+
```powershell
158+
Should -Invoke Write-CIAnnotation -ModuleName SecurityHelpers -Times 1 -Exactly
159+
Should -Invoke Write-CIAnnotation -ModuleName SecurityHelpers -ParameterFilter {
160+
$Level -eq 'Warning'
161+
}
162+
```
163+
164+
## Test Isolation
165+
166+
**`$TestDrive`**: Pester-managed temp directory, automatically cleaned per `Describe`:
167+
168+
```powershell
169+
$testDir = Join-Path $TestDrive 'test-collection'
170+
New-Item -ItemType Directory -Path $testDir -Force
171+
```
172+
173+
**`New-TemporaryFile` with try/finally**: Manual temp file management when `$TestDrive` is insufficient:
174+
175+
```powershell
176+
$tempFile = New-TemporaryFile
177+
try {
178+
# test using $tempFile
179+
}
180+
finally {
181+
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
182+
}
183+
```
184+
185+
**`$script:` scope**: Shares state across `It` blocks within a `Describe` or `Context`:
186+
187+
```powershell
188+
BeforeAll {
189+
$script:result = Get-Something -Path 'test'
190+
}
191+
It 'Returns correct name' {
192+
$script:result.Name | Should -Be 'expected'
193+
}
194+
```
195+
196+
## Environment Save and Restore
197+
198+
Tests modifying environment variables use the `GitMocks.psm1` save/restore pattern:
199+
200+
```powershell
201+
BeforeAll {
202+
Import-Module "$PSScriptRoot/../Mocks/GitMocks.psm1" -Force
203+
}
204+
BeforeEach {
205+
Save-CIEnvironment
206+
$script:MockFiles = Initialize-MockCIEnvironment
207+
}
208+
AfterEach {
209+
Remove-MockCIFiles -MockFiles $script:MockFiles
210+
Restore-CIEnvironment
211+
}
212+
```
213+
214+
## Cleanup
215+
216+
Remove imported modules in `AfterAll` to prevent state leakage between test files:
217+
218+
```powershell
219+
AfterAll {
220+
Remove-Module SecurityHelpers -Force -ErrorAction SilentlyContinue
221+
Remove-Module GitMocks -Force -ErrorAction SilentlyContinue
222+
}
223+
```
224+
225+
## Assertion Reference
226+
227+
| Assertion | Usage |
228+
|-------------------------------|-------------------------|
229+
| `Should -Be` | Exact value equality |
230+
| `Should -BeExactly` | Case-sensitive equality |
231+
| `Should -BeTrue` / `-BeFalse` | Boolean checks |
232+
| `Should -BeNullOrEmpty` | Null or empty string |
233+
| `Should -Not -BeNullOrEmpty` | Non-null and non-empty |
234+
| `Should -Match` | Regex matching |
235+
| `Should -BeLike` | Wildcard matching |
236+
| `Should -Contain` | Collection membership |
237+
| `Should -BeOfType` | Type assertion |
238+
| `Should -HaveCount` | Collection length |
239+
| `Should -Throw` | Exception expected |
240+
| `Should -Not -Throw` | No exception expected |
241+
| `Should -BeGreaterThan` | Numeric comparison |
242+
| `Should -BeLessThan` | Numeric comparison |
243+
| `Should -Invoke` | Mock call verification |
244+
245+
## Running Tests
246+
247+
Run all tests:
248+
249+
```bash
250+
npm run test:ps
251+
```
252+
253+
Run tests for a specific directory or file:
254+
255+
```bash
256+
npm run test:ps -- -TestPath "scripts/tests/linting/"
257+
npm run test:ps -- -TestPath "scripts/tests/security/Test-DependencyPinning.Tests.ps1"
258+
```
259+
260+
After execution, check `logs/pester-summary.json` for overall status and `logs/pester-failures.json` for failure details.
261+
262+
## Complete Test Example
263+
264+
<!-- <template-complete-test> -->
265+
266+
```powershell
267+
#Requires -Modules Pester
268+
# Copyright (c) Microsoft Corporation.
269+
# SPDX-License-Identifier: MIT
270+
271+
BeforeAll {
272+
. (Join-Path $PSScriptRoot '../../linting/Invoke-Linter.ps1')
273+
Import-Module (Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1') -Force
274+
$script:FixtureRoot = Join-Path $PSScriptRoot '../Fixtures/Linting'
275+
Mock Write-Host {}
276+
}
277+
278+
Describe 'Invoke-Linter' -Tag 'Unit' {
279+
Context 'when input file is valid' {
280+
BeforeAll {
281+
$script:result = Invoke-Linter -Path (Join-Path $script:FixtureRoot 'valid.md')
282+
}
283+
284+
It 'Returns zero violations' {
285+
$script:result.Violations | Should -HaveCount 0
286+
}
287+
288+
It 'Sets status to pass' {
289+
$script:result.Status | Should -Be 'Pass'
290+
}
291+
}
292+
293+
Context 'when input file has errors' {
294+
BeforeAll {
295+
$script:result = Invoke-Linter -Path (Join-Path $script:FixtureRoot 'invalid.md')
296+
}
297+
298+
It 'Returns violations' {
299+
$script:result.Violations | Should -Not -BeNullOrEmpty
300+
}
301+
302+
It 'Includes file path in each violation' {
303+
$script:result.Violations | ForEach-Object {
304+
$_.File | Should -Not -BeNullOrEmpty
305+
}
306+
}
307+
}
308+
}
309+
310+
AfterAll {
311+
Remove-Module GitMocks -Force -ErrorAction SilentlyContinue
312+
}
313+
```
314+
315+
<!-- </template-complete-test> -->

0 commit comments

Comments
 (0)