codaamok.build.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
#region Private functions
function GetPSGalleryNextAvailableVersionNumber {
    param (
        [Parameter(Mandatory)]
        [String]$ModuleName,

        [Parameter(Mandatory)]
        [Version]$VersionToBuild
    )

    Write-Verbose "Qualifying the version number to build with is available in the PowerShell Gallery" -Verbose

    for ($i = $VersionToBuild.Build; $i -le 100; $i++) {
        if ($i -eq 100) {
            throw "You have 100 unlisted packages under the same build number? Sort your life out."
        }

        try {
            $PSGalleryModuleInfo = Find-Module -Name $ModuleName -RequiredVersion $VersionToBuild -ErrorAction "Stop"
            if ($PSGalleryModuleInfo) {
                Write-Verbose "Found module in the gallery with the same verison number, adding one to the Build number and will query the gallery again"

                $VersionToBuild = [System.Version]::New(
                    $VersionToBuild.Major,
                    $VersionToBuild.Minor,
                    $VersionToBuild.Build + $i
                )
            }
            else {
                throw "Unusually, there was no object returned or excpetion throw from Find-Module while sussing out unlisted packages"
            }
        }
        catch {
            if ($_.Exception.Message -match "No match was found for the specified search criteria") {
                Write-Verbose "Found the next available version number to build with" -Verbose
                break
            }
            else {
                throw $_
            }
        }
    }

    return $VersionToBuild
}
#endregion

#region Public functions
function Get-BuildCommands {
    <#
    .SYNOPSIS
        Auxiliary
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    param (
    )

    $Commands = @{}

    Get-Command -Module "codaamok.build" | ForEach-Object {
        $Help = Get-Help -Name $_.Name
        $Synopsis = $Help.Synopsis

        if ([String]::IsNullOrWhiteSpace($Synopsis[0])) { 
            $Commands["N/A"] += @($_.Name)
        } 
        else {
            $Commands[($Synopsis -split '\n')[0]] += @($_.Name)
        }
    }

    foreach ($Key in $Commands.Keys) {
        Write-Host $Key -ForegroundColor Blue
        foreach ($Value in $Commands[$Key]) {
            Write-Host ("- {0}" -f $Value) -ForegroundColor Green
        }
        Write-Host ""
    }
}

function Export-RootModule {
    <#
    .SYNOPSIS
        Build
        Get all of the function definition content for the module and create a single .psm1 with said content
    .DESCRIPTION
        Get all of the function definition content for the module and create a single .psm1 with said content
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String[]]$DevModulePath,

        [Parameter(Mandatory)]
        [String[]]$RootModule
    )

    $null = New-Item -Path $RootModule -ItemType "File" -Force

    foreach ($FunctionType in "Private","Public") {
        '#region {0} functions' -f $FunctionType | Add-Content -Path $RootModule

        $Files = @(Get-ChildItem $DevModulePath\$FunctionType -Filter *.ps1 -Recurse)

        foreach ($File in $Files) {
            Get-Content -Path $File.FullName | Add-Content -Path $RootModule

            # Add new line only if the current file isn't the last one (minus 1 because array indexes from 0)
            if ($Files.IndexOf($File) -ne ($Files.Count - 1)) {
                Write-Output "" | Add-Content -Path $RootModule
            }
        }

        '#endregion' -f $FunctionType | Add-Content -Path $RootModule
        Write-Output "" | Add-Content -Path $RootModule
    }
}

function Export-ScriptsToProcess {
    <#
    .SYNOPSIS
        Build
        Create a single Process.ps1 script file for all script files under ScriptsToProcess\*
    .DESCRIPTION
        Create a single Process.ps1 script file for all script files under ScriptsToProcess\*
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String[]]$Path
    ) 

    $ProcessScript = New-Item -Path $BuildRoot\build\$Script:ModuleName\Process.ps1 -ItemType "File" -Force
    $Files = @(Get-ChildItem $Path -Filter *.ps1)

    foreach ($File in $Files) {
        Get-Content -Path $File.FullName | Add-Content -Path $ProcessScript

        # Add new line only if the current file isn't the last one (minus 1 because array indexes from 0)
        if ($Files.IndexOf($File) -ne ($Files.Count - 1)) {
            Write-Output "" | Add-Content -Path $ProcessScript
        }
    }
}

function Export-UnreleasedNotes {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String]$Path,

        [Parameter(Mandatory)]
        [PSCustomObject]$ChangeLogData,

        [Parameter()]
        [Bool]$NewRelease
    )

    $EmptyChangeLog = $true

    $ReleaseNotes = foreach ($Property in $ChangeLogData.Unreleased[0].Data.PSObject.Properties.Name) {
        $Data = $ChangeLogData.Unreleased[0].Data.$Property

        if ($Data) {
            $EmptyChangeLog = $false

            Write-Output ("# {0}" -f $Property)

            foreach ($item in $Data) {
                Write-Output ("- {0}" -f $item)
            }
        }
    }

    if ($EmptyChangeLog -eq $true -Or $ReleaseNotes.Count -eq 0) {
        if ($NewRelease.IsPresent) {
            throw "Can not build with empty Unreleased section in the change log"
        }
        else {
            $ReleaseNotes = "None"
        }
    }

    Write-Verbose "Release notes:" -Verbose
    $ReleaseNotes | Write-Verbose -Verbose

    Set-Content -Value $ReleaseNotes -Path $Path -Force
}

function Get-BuildVersionNumber {
    <#
    .SYNOPSIS
        Build
        Qualify the next version number to build with
    .DESCRIPTION
        Qualify the next version number to build with
    .EXAMPLE
        PS C:\> Get-BuildVersionNumber -ModuleName "PSShlink" -ManifestData $ManifestData -ChangeLogData $ChangeLogData
    #>

    param (
        [Parameter(Mandatory)]
        [String]$ModuleName,

        [Parameter(Mandatory, ParameterSetName='DetermineNextVersion')]
        [Hashtable]$ManifestData,

        [Parameter(Mandatory, ParameterSetName='DetermineNextVersion')]
        [PSCustomObject]$ChangeLogData,

        [Parameter(Mandatory, ParameterSetName='HardCodeNextVersion')]
        [Version]$VersionToBuild,

        [Parameter(ParameterSetName='DetermineNextVersion')]
        [Switch]$NewRelease
    )

    # Get PowerShell Gallery current verison number (if published)
    try {
        $PSGalleryModuleInfo = Find-Module -Name $ModuleName -ErrorAction "Stop"
    }
    catch {
        if ($_.Exception.Message -notmatch "No match was found for the specified search criteria") {
            throw $_
        }
        else {
            $PSGalleryModuleInfo = [PSCustomObject]@{
                "Name"    = $ModuleName
                "Version" = "0.0"
            }
        }
    }

    Write-Verbose ("PowerShell Gallery verison: {0}" -f $PSGalleryModuleInfo.Version) -Verbose
    Write-Verbose ("Changelog version: {0}" -f $ChangeLogData.Released[0].Version) -Verbose
    Write-Verbose ("Manifest version: {0}" -f $ManifestData.ModuleVersion) -Verbose

    if (-not $VersionToBuild) {
        if ($NewRelease.IsPresent) {
            # Try and piece together an understanding from the module manifest, PowerShell Gallery, and the change log, as to what the next version number should be

            # If the last released version in the change log and latest version available in the PowerShell gallery do not match, throw an exception - get them level!
            if ($null -ne $ChangeLogData.Released[0].Version -And $ChangeLogData.Released[0].Version -ne $PSGalleryModuleInfo.Version) {
                throw "The latest released version in the changelog does not match the latest released version in the PowerShell gallery"
            }
            # If module isn't yet published in the PowerShell gallery, and there's no Released section in the change log, set initial version as per the manifest
            elseif ($PSGalleryModuleInfo.Version -eq "0.0" -And $ChangeLogData.Released.Count -eq 0) {
                Write-Verbose "Module is not published to the PowerShell Gallery and there is not a Released section in the change log. Will use version from the module manifest." -Verbose
                $VersionToBuild = [System.Version]$ManifestData.ModuleVersion
            }
            # If module isn't yet published in the PowerShell gallery, and there is a Released section in the change log, update version
            elseif ($PSGalleryModuleInfo.Version -eq "0.0" -And $ChangeLogData.Released.Count -ge 1) {
                Write-Verbose "Module is not published to the PowerShell Gallery and there is a Released secton in the change log. Will +1 on the minor build from the changelog version." -Verbose
                $CurrentVersion = [System.Version]$ChangeLogData.Released[0].Version
                $VersionToBuild = [System.Version]::New(
                    $CurrentVersion.Major,
                    $CurrentVersion.Minor + 1,
                    $CurrentVersion.Build
                )
            }
            # If the module's PowerShell Gallery version and the last Released verison in the change log are in harmony, update version
            elseif ($ChangeLogData.Released[0].Version -eq $PSGalleryModuleInfo.Version) {
                Write-Verbose "Module is published to the PowerShell Gallery and its version matches the last Releases section in the changelog. Will +1 on the mintor build from the PowerShell Gallery version." -Verbose
                $CurrentVersion = [System.Version]$PSGalleryModuleInfo.Version
                $VersionToBuild = [System.Version]::New(
                    $CurrentVersion.Major,
                    $CurrentVersion.Minor + 1,
                    $CurrentVersion.Build
                )
            }
            else {
                Write-Output ("Latest release version from change log: {0}" -f $ChangeLogData.Released[0].Version)
                Write-Output ("Latest release version from PowerShell gallery: {0}" -f $PSGalleryModuleInfo.Version)
                throw "Can not determine next version number"
            }

            # Loop through and suss out any unlisted packages for the module in the PowerShell Gallery using the same version number
            # Keep looping and bumping the build version number by 1 until an available version number is found
            # Try this process up to 100 times and fail if can't find one
            # This can execute even if the module is not yet in the gallery because unlisted packages can still be present
            $VersionToBuild = GetPSGalleryNextAvailableVersionNumber -ModuleName $ModuleName -VersionToBuild $VersionToBuild
        }
        else {
            $VersionToBuild = [System.Version]::New(
                ([System.Version]$ManifestData.ModuleVersion).Major, 
                ([System.Version]$ManifestData.ModuleVersion).Minor, 
                ([System.Version]$ManifestData.ModuleVersion).Build + 1
            )
        }
    }
    else {
        Write-Verbose "Version to build with is hard coded" -Verbose
        if ($PSGalleryModuleInfo.Version -ne "0.0") {
            Write-Verbose "Module is published to the PowerShell Gallery" -Verbose
            $VersionToBuild = GetPSGalleryNextAvailableVersionNumber -ModuleName $ModuleName -VersionToBuild $VersionToBuild
        }
        else {
            Write-Verbose "Module not published to the PowerShell Gallery, will build with the given version number" -Verbose
        }
    }

    Write-Verbose ("Version to build: '{0}'" -f $VersionToBuild) -Verbose

    return $VersionToBuild
}

function Get-PublicFunctions {
    <#
    .SYNOPSIS
        Build
        Get a list of functions - as functions to export - defined in script files within the Public directory
    .DESCRIPTION
        Get a list of functions - as functions to export - defined in script files within the Public directory
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String[]]$Path
    )

    $Files = @(Get-ChildItem $Path -Filter *.ps1 -Recurse)

    foreach ($File in $Files) {
        $tokens = $errors = @()
        $Ast = [System.Management.Automation.Language.Parser]::ParseFile(
            $File.FullName,
            [ref]$tokens,
            [ref]$errors
        )

        if ($errors[0].ErrorId -eq 'FileReadError') {
            throw [InvalidOperationException]::new($errors[0].Message)
        }

        Write-Output $Ast.EndBlock.Statements.Name
    }
}

function Invoke-BuildClean {
    <#
    .SYNOPSIS
        Build
        Empty the contents of the build and release directories. If not exist, create them.
    .DESCRIPTION
        Empty the contents of the build and release directories. If not exist, create them.
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String[]]$Path
    )

    foreach ($item in $Path) {
        if (Test-Path $item) {
            Remove-Item -Path $item\* -Exclude ".gitkeep" -Recurse -Force
        }
        else {
            $null = New-Item -Path $item -ItemType "Directory" -Force
        }
    }
}

function New-BuildEnvironmentVariable {
    <#
    .SYNOPSIS
        Build
        Set build and platform specific environment variables.
    .DESCRIPTION
        Set build and platform specific environment variables.
    .EXAMPLE
        PS C:\> New-BuildEnvironmentVariable -Variables @{ VersionToBuild = "1.2.3" } -Platform "GitHubActions"
        
        Writes to GitHub Action's environment variable file to create environment variable "VersionToBuild" with value of "1.2.3".
    #>

    param (
        [Parameter(Mandatory)]
        [Hashtable]$Variable,

        [Parameter(Mandatory)]
        [ValidateSet("GitHubActions")]
        [String[]]$Platform
    )

    switch ($Platform) {
        "GitHubActions" {
            foreach ($var in $Variable.GetEnumerator()) {
                Write-Output ("{0}={1}" -f $var.Key, $var.Value) | Add-Content -Path $env:GITHUB_ENV 
            }
        }
    }
}

function Install-BuildModules {
    <#
    .SYNOPSIS
        Setup
        Install, or update, and import build-dependent modules
    .DESCRIPTION
        Install, or update, and import build-dependent modules
    .EXAMPLE
        PS C:\> Install-BuildModules
        
        Installs the default build modules "PlatyPS","ChangelogManagement","InvokeBuild" if they're not installed, updates them for the first run if they are installed, and finally imports them.
    #>

    [CmdletBinding()]
    param (
        [Parameter()]
        [String[]]$Module = @("PlatyPS","ChangelogManagement","InvokeBuild")
    )

    if (-not (Get-Module $Module) -And (Get-Module $Module -ListAvailable)) {
        # If installed but not imported, try and update them - good for local development, just makes the first run a little delayed
        Update-Module $Module
    }
    elseif (-not (Get-Module $Module -ListAvailable)) {
        Install-Module $Module -Scope CurrentUser -Force
    }

    Import-Module $Module -Force
}

function New-ModuleDirStructure {
    <#
    .SYNOPSIS
        Setup
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    param (
        [Parameter(Mandatory)]
        [String]$Path,
        [Parameter(Mandatory)]
        [String]$ModuleName,
        [Parameter()]
        [String]$Author = "Adam Cook (@codaamok)",
        [Parameter(Mandatory)]
        [String]$Description,
        [Parameter()]
        [String[]]$Tags,
        [Parameter()]
        [String]$ProjectUri,
        [Parameter()]
        [Switch]$CreateFormatFile,
        [Parameter()]
        [Version]$PowerShellVersion = 5.1
    )

    # Create the module and private function directories
    @(
        "$Path\$ModuleName",
        "$Path\.github\workflows"
        "$Path\$ModuleName\ScriptsToProcess",
        "$Path\$ModuleName\Files",
        "$Path\$ModuleName\Private",
        "$Path\$ModuleName\Public",
        "$Path\$ModuleName\en-US"
    ) | ForEach-Object {
        New-Item -Path $_ -ItemType Directory -Force
        New-Item -Path $_\.gitkeep -ItemType File -Force
    }

    #Create the module and related files
    $GitIgnorePath = Join-Path -Path $Path -ChildPath ".gitignore"
    $ModuleScript = "{0}.psm1" -f $ModuleName
    $ModuleScriptPath = Join-Path -Path $Path -ChildPath $ModuleScript
    $ModuleManifest = "{0}.psd1" -f $ModuleName
    $ModuleManifestPath = Join-Path -Path $Path -ChildPath $ModuleManifest
    New-Item $ModuleManifestPath -ItemType File -Force
    @(
        '$Public = @( Get-ChildItem -Path $PSScriptRoot\Public -Recurse -Filter "*.ps1" )'
        '$Private = @( Get-ChildItem -Path $PSScriptRoot\Private -Recurse -Filter "*.ps1" )'
        'foreach ($import in @($Public + $Private)) {'
        ' try {'
        ' . $import.fullname'
        ' }'
        ' catch {'
        ' Write-Error -Message "Failed to import function $($import.fullname): $_"'
        ' }'
        '}'
        'Export-ModuleMember -Function $Public.Basename'
    ) | Set-Content -Path $ModuleManifestPath -Force
    @(
        'build/*'
        'release/*'
        '!*.gitkeep'
    ) | Set-Content -Path $GitIgnorePath

    $ModuleHelp = "about_{0}.help.txt" -f $ModuleName
    $ModuleHelpPath - "{0}\{1}\en-US\{2}" -f $Path, $ModuleName, $ModuleHelp
    New-Item $ModuleHelpPath -ItemType File -Force

    $NewModuleManifestSplat = @{
        Path                = Join-Path -Path $Path -ChildPath $ModuleName | Join-Path -ChildPath $ModuleManifest
        RootModule          = $ModuleScript
        Description         = $Description
        PowerShellVersion   = $PowerShellVersion
        Author              = $Author
        FunctionsToExport   = '*'
    }

    if ($CreateFormatFile) { 
        $ModuleFormat = "{0}.Format.ps1xml" -f $ModuleName
        $ModuleFormatPath = "{0}\{1}\{2}" -f $Path, $ModuleName, $ModuleFormat
        New-Item $ModuleFormatPath -ItemType File -Force
        $NewModuleManifestSplat["FormatsToProcess"] = $ModuleFormat
    }

    if ($ProjectUri) {
        $NewModuleManifestSplat["ProjectUri"] = $ProjectUri
    }

    New-ModuleManifest @NewModuleManifestSplat

    # Copy the public/exported functions into the public folder, private functions into private folder

}

function New-ProjectDirStructure {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String]$Path,

        [Parameter(Mandatory)]
        [String]$Name,

        [Parameter()]
        [String]$Platform
    )

    # TODO create CHANGELOG.md, copy github action workflow and build script, create module dir structure
}

function New-VSCodeTaskFile {
    <#
    .SYNOPSIS
        Setup
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

}

function Update-BuildFiles {
    <#
    .SYNOPSIS
        Setup
        Copy the build files (script + GitHub Actiosn workflow) from the module's install directory to the specified directory
    .DESCRIPTION
        Copy the build files (script + GitHub Actiosn workflow) from the module's install directory to the specified directory
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String]$DestinationPath
    )

    $Module = Get-Module "codaamok.build"

    # Check for files within the ModuleBase directory aswell the Files subfolder in case this command is being used during development of codaamok.build itself
    @(
        [PSCustomObject]@{
            File = "{0}\invoke.build.ps1" -f $Module.ModuleBase
            DestinationPath = $DestinationPath
        },
        [PSCustomObject]@{
            File = "{0}\Files\invoke.build.ps1" -f $Module.ModuleBase
            DestinationPath = $DestinationPath
        },
        [PSCustomObject]@{
            File = "{0}\deploy-powershellgallery.yml" -f $Module.ModuleBase
            DestinationPath = "{0}\.github\workflows" -f $DestinationPath
        },
        [PSCustomObject]@{
            File = "{0}\Files\deploy-powershellgallery.yml" -f $Module.ModuleBase
            DestinationPath = "{0}\.github\workflows" -f $DestinationPath
        }
    ) | ForEach-Object {
        if (Test-Path $_.File) {
            New-Item -Path $_.DestinationPath -ItemType "Directory" -Force
            Copy-Item -Path $_.File -Destination $_.DestinationPath -Confirm
        }
    }
}
#endregion