如何发布powershell命令到gallery(超详细教程,一文带你跑通全流程)

47 阅读5分钟

经典背景介绍:这一切的背景还是因为最近实习离职了之后用回了windows开发,windows上面的tree命令感觉实在是不好用,因为不能控制-L深度,几番求索之后没找到一个喜欢的命令,最后决定是自己写一个工具命令,经过调试之后觉得还不错挺好用的,然后我就在想能不能跟npm包一样发布到大家都可用的那种社区平台上,然后我找到了powershell gallery.

创建Powershell Gallery账户

点此创建账户👉:PowerShellGallery.com

看到这里就说明创建完成:

然后点击用户名选择API Keys,创建一个新的api-key:

配置key

然后get到key:

使用PSScriptAnalyzer

这是微软官方文档建议完成的优先级第一的步骤: 使用PSScriptAnalyzer识别PowerShell代码中最常见的问题,并且通常会提供有关如何解决问题的建议。

1. 安装 PSScriptAnalyzer

Install-Module PSScriptAnalyzer -Scope CurrentUser

查看版本:

Get-Module PSScriptAnalyzer -ListAvail

2. 对脚本进行扫描

比如这是我的脚本路径:

D:\TreeView-PS\TreeView.ps1
Invoke-ScriptAnalyzer -Path "D:\TreeView-PS\TreeView.ps1"

会输出类似:

RuleName                            Severity Line Message
--------                            -------- ---- -------
PSAvoidUsingWriteHost               Warning  33  FileTree.cmd uses Write-Host; use Write-Output instead.
PSUseDeclaredVarsMoreThanAssignments Warning ...
...

扫描pester版本

参考这里的问题把问题都解决。直到使用扫描命令无输出。

3. 对整个模块扫描

如果你组织成模块结构:

TreeView/
  TreeView.psd1
  TreeView.psm1

运行:

Invoke-ScriptAnalyzer -Path "TreeView" -Recurse

4. 自动修复(如果可能)

Invoke-ScriptAnalyzer -Path .\TreeView.ps1 -Fix

不是所有规则都能自动修,但能自动 fix 的就会帮你处理。

5. 发布前自动检查

可以在发布前先跑:

Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning,Error

如果未来用 GitHub Actions 发布模块,可以加:

.github/workflows/lint.yml

name: Lint PowerShell

on: [push, pull_request]

jobs:
  analyze:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install PSScriptAnalyzer
        run: Install-Module PSScriptAnalyzer -Force -Scope CurrentUser
      - name: Run Analyzer
        run: Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning,Error

使用Pester进行单元测试

安装现代版本的Pester

Windows 10 / Windows Server 2016 及更高版本预装了 Pester 3.4.0 版本。此内置版本无法使用简单的Update-Modulecmdlet 进行更新。您需要执行并行安装才能开始使用最新版本的 Pester。

内置版本由微软签名,而较新的版本由社区维护并使用不同的证书签名,这Install-Module有时会导致出现错误,要求我们接受新的发布者证书。

Installation and Update | Pester

首先安装Pester 5.x 现代版本,由于系统里预置了不再更新的3.x, 所以安装新版应当使用参数跳过系统检查:

Install-Module -Name Pester -Force -Scope CurrentUser -SkipPublisherCheck

安装完成之后可以用此命令验证版本:

Get-Module -ListAvailable Pester

看到有了5.7.1就说明就绪了,然后最好是卸载已加载的Pester3,否则不会用5.x版本,运行下面命令卸载靠前的版本并手动加载新版(5.0.0以后的版本):

Remove-Module Pester -Force -ErrorAction SilentlyContinue
Import-Module Pester -MinimumVersion 5.0.0 -Force

现在再看一下当前模块版本:

(Get-Module Pester).Version
PS D:\TreeView-PS> (Get-Module Pester).Version

Major  Minor  Build  Revision
-----  -----  -----  --------
5      7      1      -1

执行测试

然后准备好测试脚本,项目目录如:

D:\TreeView-PS
├── 📁 Private                    # 私有方法,不导出
│   ├── 📄 AnsiColors.ps1
│   ├── 📄 Get-Color.ps1
│   ├── 📄 Get-Icon.ps1
│   ├── 📄 Show-Tree.ps1
│   └── 📄 Write-OutOrHost.ps1
├── 📁 Public
│   └── 📄 TreeView.ps1           # 主函数
├── 📁 Tests
│   └── 📄 TreeView.Tests.ps1
├── 📄 TreeView.psd1
└── 📄 TreeView.psm1

在仓库根目录执行:

# 运行目录下所有测试
Invoke-Pester -Path .\Tests -Output Detailed

效果如下:

一个测试内部模块未导出的function的方法

在真实模块设计中,不推荐导出内部函数(Private Functions)。 例如 Get-Icon / Get-Color 这类内部辅助函数不应该成为模块的对外 API。

如果你的测试脚本里试图访问模块内的函数但却没有导出,可能会出现如下报错:

CommandNotFoundException: 无法将“Get-Color”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。

可以采用这个办法--Dot Source

BeforeAll {
    # 导入模块
    Import-Module "$PSScriptRoot\..\TreeView.psm1" -Force

    # dot-source 私有函数,仅测试用
    . "$PSScriptRoot\..\Private\Get-Icon.ps1"
    . "$PSScriptRoot\..\Private\Get-Color.ps1"
}

测试覆盖率报告

如需同时生成覆盖率,可以参考这个文档:Generating Code Coverage Metrics | Pester

贴一个例子:

# Create a Pester configuration object using `New-PesterConfiguration`
$config = New-PesterConfiguration

# Set the test path to specify where your tests are located. In this example, we set the path to the current directory. Pester will look into all subdirectories.
$config.Run.Path = ".\Tests"
 
# Enable Code Coverage
$config.CodeCoverage.Enabled = $true
 
$config.CodeCoverage.Path = "."

# Run Pester tests using the configuration you've created
Invoke-Pester -Configuration $config

我查看了一下为自己的覆盖率:

Starting discovery in 1 files.
Discovery found 10 tests in 88ms.
Starting code coverage.
Running tests.
[+] TreeView.Tests.ps1 995ms (543ms|369ms)
Tests completed in 1.01s
Tests Passed: 10, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
Processing code coverage result.
Covered 37.18% / 75%. 234 analyzed Commands in 10 Files.

可以看到我这个分支覆盖率还很低,不过这并不是本博客的重点,至于提高覆盖率可以通过其他的手段来实现。

推送到Powershell Gallery

注册 PowerShell Gallery API Key

还记得我们之前是有得到了一个api-key的,你应当已经将这个api-key给记在某处。

生成psd1(打包)

使用下列命令:

New-ModuleManifest -Path ./TreeView.psd1 `
    -RootModule TreeView.psm1 `
    -ModuleVersion "1.0.0" `
    -Author "你的名字" `
    -Description "A tree command for Windows with icons and depth support."

生成的psd1内容像这样:

@{
    RootModule        = 'TreeView.psm1'
    ModuleVersion     = '1.0.0'
    GUID              = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
    Author            = '你'
    CompanyName       = '你'
    Description       = 'Linux tree-like directory viewer with icon mode.'
    PowerShellVersion = '5.1'
    FunctionsToExport = @('TreeView')
}

给他改吧改吧,把信息填一填.

你可以选择手动发布(简单好操作)

Publish-Module -Path .\ -Repository PSGallery -NuGetApiKey '<your-api-key>'

Publish-Module命令详解

1. Publish-Module

PowerShell 内置的发布命令,用来把一个模块上传到 PowerShell Gallery。

一般你的模块目录结构如下:

TreeView-PS/
  TreeView.psd1
  TreeView.psm1
  Public/
  Private/

只要有 .psd1 就可以发布。

2. -Path .

代表“当前路径就是我要发布的模块目录”。

你的执行目录一般就是模块根目录,所以写 . 就够了。

如果放模块在子目录,可能要写成:-Path ./src/TreeView

3. -Repository PSGallery

指定发布目标仓库,就是官方 PowerShell Gallery。

你也可以发布到私有 NuGet 服务器,但你现在用的是默认的公共仓库。

4. -NuGetApiKey <your-api-key>

这是发布到 Gallery 的 登录凭证

PowerShell Gallery 使用 NuGet API Key 鉴权,也就是我们在Powershell Gallery申请的key。

所以整句命令的意思是:

使用 GitHub Actions 自动将当前目录的 PowerShell 模块上传到 PowerShell Gallery, 使用存放在环境变量中的 NuGet API Key 认证,并发布。

使用GitHub Actions实现CI/CD(推荐)

CI/CD 代表持续集成 (Continuous Integration)和持续交付/部署 (Continuous Delivery/Deployment),是一种软件开发方法,通过自动化构建、测试和部署流程,实现更快、更频繁地向客户交付高质量的软件。持续集成 (CI) 侧重于开发人员频繁将代码集成到共享存储库并进行自动化测试;而持续交付/部署 (CD) 则在此基础上,自动化将变更交付到生产环境的过程。

若要使 GitHub 在存储库中发现任何 GitHub Actions 工作流,必须将工作流文件保存在名为 .github/workflows 的目录中。

你可以为工作流文件指定所需的任何名称,但必须使用 .yml 或 .yaml 作为文件扩展名。 YAML 通常是用于配置文件的标记语言。 -- Github官方文档

Github建仓

  1. 在Github上创建仓库

建一个仓库记住url,如:https://github.com/你的账号/<仓库名字>.git

  1. 本地初始化git并推送
git init                  # 如果还没初始化过
git add .
git commit -m "Initial commit"
git branch -M main         # 保证主分支是 main
git remote add origin https://github.com/你的账号/<仓库名字>.git
git push -u origin main

使用Github Action

你需要先来到网页端,给你的仓库配置一个专用的 API Key。创建方法如图:

这个时候就把刚刚存的API Key拿出来就行,然后给他命名,以后用得上,我这里命名为:

PSGalleryApiKey

在项目根目录下创建./github/workflows/main.yml,github的教程提供了一个模板,下面以我的方法为例解释ci/cd 工作流文件:

name: PowerShell Module CI/CD

on:
    push:
        branches:
            - main
    pull_request:

jobs:
    test-and-publish:
        runs-on: windows-latest

        steps:
            - name: Checkout repo
              uses: actions/checkout@v3

            - name: Setup PowerShell
              uses: actions/setup-powershell@v3

            - name: Install Pester
              run: Install-Module -Name Pester -Force -Scope CurrentUser

            - name: Run Pester tests
              run: |
                  Invoke-Pester -Path .\Tests -Output Detailed

            - name: Update ModuleVersion
              shell: pwsh
              run: |
                $psd1Path = ".\TreeView.psd1"
                $psd1 = Get-Content $psd1Path
                $newVersion = "1.0.$env:GITHUB_RUN_NUMBER"
                $psd1 = $psd1 -replace "ModuleVersion\s*=\s*'.*?'", "ModuleVersion = '$newVersion'"
                Set-Content $psd1Path $psd1
              env:
                GITHUB_RUN_NUMBER: ${{ github.run_number }}

            - name: Publish to PSGallery
              if: github.ref == 'refs/heads/main' && github.event_name == 'push'
              env:
                  <YourKey>: ${{ secrets.<YourKey> }}
              run: |
                  Publish-Module -Path . -NuGetApiKey $env:<YourKey> -Repository PSGallery -Force
  1. checkout:拉取仓库代码
  2. setup PowerShell:确保 Windows runner 安装 PowerShell
  3. 安装 Pester:最新版本
  4. 运行单元测试:确保模块没有问题
  5. 发布模块:
    1. 只在 main 分支 push 时触发
    2. 使用仓库 Secrets 中的 PSGalleryApiKey
    3. -Force 会覆盖同版本模块,如果你想严格控制版本可以改成自动生成新版本

push后自动执行流程

如下图,找到正在执行的workflow:

等待执行成功或失败。

然后我们可以看到这里有一个失败是啥呢,是这个-f支路没有成功:

No valid module was found

Line |
   2 |  Publish-Module -Path . -NuGetApiKey $env:PSGalleryApiKey -Repository  …
     |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | The specified module with path '.' was not published because no valid module was found with that path.

这个报错是因为仓库无合法模块,注意合法的ps模块必须是这样的结构,你只能用子文件夹作为目录,不能用根:

<ModuleName>/
  <ModuleName>.psd1
  <ModuleName>.psm1

只要把Publish-Module -Path . -NuGetApiKey $env:PSGalleryApiKey -Repository PSGallery -Force改成Publish-Module -Path ./<结构正确的子文件夹> -NuGetApiKey $env:PSGalleryApiKey -Repository PSGallery -Force

经过坚持不懈的调试终于发布成功。

参考文档

ps!推荐一个可以用来快速获得md link的浏览器插件:Copy as Markdown

PowerShell 库发布指南和最佳做法 - PowerShell | Microsoft Learn

PowerShell - 如何在 Pester v5 中处理点源? - Stack Overflow

Generating Code Coverage Metrics | Pester

构建和测试 PowerShell - GitHub 文档