Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `copilot-instructions.md` documenting build/test/quality commands, the mandatory `./build.ps1` entry point, high-level architecture, key conventions (changelog discipline, `-ErrorAction 'Ignore'` preference, cross-platform/cross-version PS5.1+PS7 support), guidance for running long builds without hanging the agent shell (always tee output to a log file), and how to extract test failures from the Pester NUnit XML and HQRM CliXml result files.
- Per-area `instructions/*.instructions.md` files for public functions, private functions, Plaster templates, build tasks, and Pester tests.
- `agents/sampler-maintainer.md` agent definition and the `skills/validate-changes` skill that picks the smallest useful test scope and surfaces XML-based failure diagnostics.
- Tab-completion for the `-Tasks` parameter of the scaffolded `Build.ps1`.
Candidates include the Invoke-Build `?` help token, tasks defined under
`./.build/**/*.ps1`, tasks imported from modules under
`./output/RequiredModules/**/*.build.ps1`, and workflow aliases declared
under `BuildWorkflow:` in `build.yaml`. The completer is self-contained and
works in a fresh clone before `Resolve-Dependency` has ever run.

### Changed

Expand Down Expand Up @@ -993,7 +999,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Fixed #192 where the `Build-Module` command from module builder returns
a rooted path (sometimes).
a rooted path (sometimes).

## [0.107.0] - 2020-09-07

Expand Down
2 changes: 1 addition & 1 deletion GitVersion.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mode: ContinuousDelivery
next-version: 0.90
next-version: 0.90.0
Comment thread
raandree marked this conversation as resolved.
major-version-bump-message: '\s?(breaking|major|breaking\schange)'
minor-version-bump-message: '(adds?|features?|minor)\b'
patch-version-bump-message: '\s?(fix|patch)'
Expand Down
75 changes: 75 additions & 0 deletions Sampler/Templates/Build/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,81 @@
param
(
[Parameter(Position = 0)]
[ArgumentCompleter({
param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

# $PSScriptRoot is not reliable inside an ArgumentCompleter script block;
# derive the script root defensively, falling back to the current location.
$scriptRoot = if ($PSCommandPath)
{
Split-Path -Path $PSCommandPath -Parent
}
else
{
$PWD.Path
}

$tasks = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
Comment thread
raandree marked this conversation as resolved.
Outdated

# Built-in Invoke-Build help token
[void] $tasks.Add('?')

$taskRegex = '^\s*task\s+[''"]?([A-Za-z_][\w\.\-]*)'

# Tasks defined in the local '.build' directory
$localBuildDir = Join-Path -Path $scriptRoot -ChildPath '.build'
if (Test-Path -Path $localBuildDir)
{
Get-ChildItem -Path $localBuildDir -Filter '*.ps1' -Recurse -ErrorAction SilentlyContinue |
Select-String -Pattern $taskRegex -ErrorAction SilentlyContinue |
ForEach-Object { [void] $tasks.Add($_.Matches[0].Groups[1].Value) }
}

# Tasks imported from Sampler and related modules (*.build.ps1 files)
$modulesDir = Join-Path -Path $scriptRoot -ChildPath 'output/RequiredModules'
if (Test-Path -Path $modulesDir)
{
Get-ChildItem -Path $modulesDir -Filter '*.build.ps1' -Recurse -ErrorAction SilentlyContinue |
Select-String -Pattern $taskRegex -ErrorAction SilentlyContinue |
ForEach-Object { [void] $tasks.Add($_.Matches[0].Groups[1].Value) }
}

# Workflow aliases defined under BuildWorkflow in build.yaml
$buildYaml = Join-Path -Path $scriptRoot -ChildPath 'build.yaml'
if (Test-Path -Path $buildYaml)
{
$inWorkflow = $false
foreach ($line in Get-Content -LiteralPath $buildYaml -ErrorAction SilentlyContinue)
Comment thread
raandree marked this conversation as resolved.
Outdated
{
if ($line -match '^BuildWorkflow\s*:')
{
$inWorkflow = $true
continue
}

if ($inWorkflow)
{
# Exit the BuildWorkflow block as soon as a new top-level key is encountered
if ($line -match '^\S')
{
break
}

if ($line -match '^\s{2}["'']?([^"''\s:#][^:]*?)["'']?\s*:\s*$')
{
[void] $tasks.Add($Matches[1])
}
}
}
}

$tasks |
Where-Object { $_ -like "$wordToComplete*" } |
Sort-Object |
ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
})]
[System.String[]]
$Tasks = '.',

Expand Down
122 changes: 122 additions & 0 deletions tests/Unit/Templates/Build.ps1.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
Describe 'Sampler Build.ps1 template' {
BeforeAll {
$templatePath = Join-Path -Path $PSScriptRoot -ChildPath '../../../Sampler/Templates/Build/build.ps1'
$templatePath = (Resolve-Path -Path $templatePath).Path

$tokens = $null
$parseErrors = $null
$script:ast = [System.Management.Automation.Language.Parser]::ParseFile(
$templatePath, [ref] $tokens, [ref] $parseErrors
)
$script:parseErrors = $parseErrors

$script:tasksParam = $script:ast.FindAll({
param ($node)
$node -is [System.Management.Automation.Language.ParameterAst] -and
$node.Name.VariablePath.UserPath -eq 'Tasks'
}, $true) | Select-Object -First 1

$script:completerAttr = $null
if ($script:tasksParam)
{
$script:completerAttr = $script:tasksParam.Attributes |
Where-Object { $_.TypeName.FullName -eq 'ArgumentCompleter' } |
Select-Object -First 1
}
}

It 'Parses without errors' {
$script:parseErrors.Count | Should -Be 0
}

It 'Defines a $Tasks parameter' {
$script:tasksParam | Should -Not -BeNullOrEmpty
}

It 'Has an ArgumentCompleter attribute on $Tasks' {
$script:completerAttr | Should -Not -BeNullOrEmpty
}

Context 'When invoking the ArgumentCompleter against a fake workspace' {
BeforeAll {
$script:fakeRoot = Join-Path -Path $TestDrive -ChildPath 'FakeProject'
$null = New-Item -ItemType Directory -Path $script:fakeRoot
$null = New-Item -ItemType Directory -Path (Join-Path -Path $script:fakeRoot -ChildPath '.build')

Set-Content -Path (Join-Path -Path $script:fakeRoot -ChildPath '.build/Foo.ps1') -Value 'task Foo {}'

$yamlLines = @(
Comment thread
raandree marked this conversation as resolved.
'BuildWorkflow:'
' CompWorkflow:'
' - build'
' - test'
' OtherWf:'
' - noop'
'NextKey: value'
)
Set-Content -Path (Join-Path -Path $script:fakeRoot -ChildPath 'build.yaml') -Value ($yamlLines -join [System.Environment]::NewLine)

$fakeScriptPath = Join-Path -Path $script:fakeRoot -ChildPath 'Build.ps1'
Set-Content -Path $fakeScriptPath -Value '# stub'

$script:completerSource = $script:completerAttr.PositionalArguments[0].ScriptBlock.GetScriptBlock().ToString()

# Run the completer in a fresh runspace with the fake workspace as
# the current location. Because no script is being invoked,
# $PSCommandPath is empty inside the runspace and the completer
# falls back to $PWD.Path for its script root.
function Invoke-Completer
{
param ([string] $WordToComplete)
Comment thread
raandree marked this conversation as resolved.
Outdated

$ps = [powershell]::Create()
try
{
$null = $ps.AddScript("Set-Location -LiteralPath '$($script:fakeRoot.Replace("'", "''"))'")
$null = $ps.Invoke()
$ps.Commands.Clear()

$null = $ps.AddScript("`$completer = [scriptblock]::Create(@'`n$($script:completerSource)`n'@); & `$completer 'x' 'Tasks' '$WordToComplete' `$null `$null")
return $ps.Invoke()
}
finally
{
$ps.Dispose()
}
}
}

It 'Returns a non-empty, sorted, deduplicated completion list' {
$results = Invoke-Completer -WordToComplete ''
$values = @($results | ForEach-Object { $_.CompletionText })

$values | Should -Not -BeNullOrEmpty
$values | Should -Contain '?'
$values | Should -Contain 'Foo'
$values | Should -Contain 'CompWorkflow'
$values | Should -Contain 'OtherWf'

# Sorted alphabetically.
Comment thread
raandree marked this conversation as resolved.
Outdated
($values -join '|') | Should -Be (($values | Sort-Object) -join '|')

# Deduplicated (case-insensitive).
($values | Group-Object | Where-Object Count -gt 1) | Should -BeNullOrEmpty
}

It 'Filters results based on $wordToComplete' {
$results = Invoke-Completer -WordToComplete 'Comp'
$values = @($results | ForEach-Object { $_.CompletionText })

$values | Should -Contain 'CompWorkflow'
$values | Should -Not -Contain 'Foo'
$values | Should -Not -Contain 'OtherWf'
}

It 'Emits CompletionResult objects with ParameterValue result type' {
$results = @(Invoke-Completer -WordToComplete '')
$results[0] | Should -BeOfType ([System.Management.Automation.CompletionResult])
$results[0].ResultType | Should -Be ([System.Management.Automation.CompletionResultType]::ParameterValue)
}
}
}

Loading