からめもぶろぐ。

俺たちは雰囲気で OAuth をやっている

PowerShell モジュールを Azure Artifacts に公開する

この記事は Azure DevOps Advent Calendar 2019 に参加しています。

qiita.com

Azure DevOps には Azure Artifacts というパッケージをホストする機能があります。Azure Artifacts では NuGet パッケージをサポートするので、PowerShell モジュールを公開してプライベートなリポジトリとして使用することができます。Azure Pipelines でPowerShell モジュールをビルドして Azure Artifacts に公開するまでの手順を見てみたいと思います。

手順

プロジェクトの準備

今回作成する PowerShell モジュールは PowerShell Core を前提としますので、.NET Core 2.1 でビルドする「HelloPowerShell」という名前のプロジェクトを作成します。モジュールのマニフェスト ファイル (.psd1) と NuGet のマニフェスト ファイル (.nuspec) を作成し、ビルド時にファイルを出力ディレクトリにコピーするように設定しておきます。作成したプロジェクトは Azure Repos の Git に push します。

Build Pipeline の作成

Build Pipeline を作成します。[Azure Repos Git] からリポジトリを選択し [Starter pipeline] で最小テンプレートの YAML を作成します。作成した YAML を以下のように設定します。

trigger:
- master

pool:
  vmImage: 'windows-latest'

variables:
  solutionFile: 'HelloPowerShell.csproj'
  manifestFile: 'HelloPowerShell.psd1'
  nuspecFile: 'HelloPowerShell.nuspec'
  buildConfiguration: 'Release'
  buildVersion: '1.0.0'
  buildId: '$(Build.BuildId)'

steps:
- task: PowerShell@2
  inputs:
    targetType: 'inline'
    script: |
      Write-Host "##vso[task.setvariable variable=buildNumber]$env:buildVersion.$env:buildId"
- task: PowerShell@2
  inputs:
    targetType: 'inline'
    script: |
      Update-ModuleManifest -Path $env:manifestFile -ModuleVersion $env:buildNumber
- task: PowerShell@2
  inputs:
    targetType: 'inline'
    script: |
      $xml=[xml](Get-Content $env:nuspecFile)
      $xml.package.metadata.version=$env:buildNumber
      $xml.Save($env:nuspecFile)
- task: DotNetCoreCLI@2
  inputs:
    command: 'restore'
    projects: '$(solutionFile)'
- task: DotNetCoreCLI@2
  inputs:
    command: 'publish'
    publishWebProjects: false
    projects: '$(solutionFile)'
    arguments: '-c $(buildConfiguration) -o $(Build.ArtifactStagingDirectory) -p:version=$(buildNumber);fileversion=$(buildNumber)'
    zipAfterPublish: false
- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'HelloPowerShell'
    publishLocation: 'Container'

dotnet publish で Artifact を作成するだけですが、バージョンがビルドごとに変わるようにバージョン番号の書き換えを行っています。

Release Pipeline の作成

続いて Release Pipeline を作成します。Build Pipeline の Artifact をトリガーに設定し CI/CD を有効にします。

ジョブでは NuGet をインストールし、nuget pack と nuget push を実行します。

nuget pack では、対象を .csproj ではなく .nuspec に変更します。

nuget push では push するフィードに Azure Artifacts のフィードを指定します。


実行

Pipeline を実行すると、Azure Artifacts にパッケージが追加されることがわかります。

あとは Register-PSRepository でフィードを登録することで、Find-Module や Install-Module などのコマンドレットが使用できるようになります。

docs.microsoft.com

Windows PowerShell と PowerShell Core に対応した PowerShell モジュールを作成する

この記事は PowerShell Advent Calendar 2019 に参加しています。

qiita.com

PowerShell の新しいバージョンである PowerShell Core が提供されて久しいですが、PowerShell モジュールを開発して一般に提供するような場合、互換性を考えて、旧来の Windows PowerShell をサポートしなければならないことがあります。Windows PowerShell は .NET Framework で動作し、PowerShell Core は .NET Core で動作するので、両方に対応したモジュールを作成するにはちょっとした工夫が必要です。

モジュールを PowerShell スクリプトで記述する

PowerShell スクリプト (この場合は .psm1) は実行される PowerShell のバージョンの影響を受けないため、Windows PowerShell と PowerShell Core のどちらでも動作します。ただし PowerShell Core での破壊的変更には注意する必要があります。

docs.microsoft.com

また、PowerShell スクリプトが外部のライブラリ (.dll) に依存する場合、そのライブラリが .NET Standard で作られている必要があります。

マニフェスト ファイルで動的にモジュールを読み込む

モジュールをバイナリ (.dll) で開発する場合、マニフェスト ファイル (.psd1) では RootModule の宣言に対して関数を受けられるようになっており、その中で $PSEdition を判断して読み込むモジュールを変更することができます。.NET Framework と .NET Core の両方でビルドする必要はありますが、安全にモジュールを読み込むことができます。詳細については以下に記載されています。

docs.microsoft.com

モジュールを .NET Standard で作成する

モジュールを .NET Standard (netstandard2.0) でビルドすることで Windows PowerShell と PowerShell Core の両方で動作させることができます。*1 この方法の一番の問題は PowerShell のコア ライブラリである System.Management.Automation.dll の参照になるのですが、以下のように GAC をパス指定で強引に読み込むことで解決します。実行時には必ず System.Management.Automation.dll は読み込まれているので、問題になることはありません。

<ItemGroup>
  <Reference Include="System.Management.Automation">
    <HintPath>C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll</HintPath>
    <Private>false</Private>
  </Reference>
</ItemGroup>

この場合も、依存関係のある外部のライブラリについても .NET Standard 化されていないと厳しいと思います。

(2019/12/25 追記)
コメントいただきました。PowerShellStandard.Library を参照するのが正しい方法とのことです。

まとめ

依存関係もまとめて解決できるので、マニフェスト ファイルで制御するのが最も確実な方法になります。とはいえ、他の方法も悪いというわけではないので、状況に応じて使っていただければよいかと思います。

*1:これは Az モジュールが使っている方法になります。

Global Microsoft 365 Developer Bootcamp 2019 Tokyo に登壇しました

2019/11/23 に開催された Global Microsoft 365 Developer Bootcamp 2019 Tokyo で Adaptive Cards のハンズオン担当として登壇しました。

connpass.com

「ファイル名を間違えないように」と言っておきながら、自分がファイル名を間違えるという凡ミスがありましたが、最後までハンズオンをやり切れたのでよかったです。
割と早めに資料を作り上げていたのですが、当日までに微妙なアップデートがあって (Azure ポータルの見た目が変わってる、Flow が Power Automate に名称変更している、.NET Core 3.0 が GA している、などなど) いろいろご迷惑をおかけしました。ちゃんと本番前に資料の見直しをしないといけないですね。

スライドは Speaker Deck にアップロードしています。

CSOM で通知を取得するときの仕様が鬼畜な件について

SharePoint にはリスト アイテムに変更があったときにメールで通知をしてくれる機能があります。上位互換の Power Automate を使うという方法もありますが、モダン UI にもちゃんとメニューがあって、組み込みのお手軽さもあるため、まだまだ使う機会は多いでしょう。

CSOM では Web.Alerts プロパティでサイトに設定されている通知の一覧を取得することができます。しかし思わぬところでうまくいかないことがありますのでご紹介しておきたいと思います。

ItemID プロパティを Load に含めるとエラーになる

通知にはリスト全体に設定するものと特定のリスト アイテムのみに設定するものがあります。それぞれは ListID プロパティと ItemID プロパティによって特定することができます。
試しに ListID プロパティと ItemID プロパティを取得してみたいと思います。それぞれは既定では読み込まれないので明示的に指定してあげる必要があります。

private static void Main(string[] args)
{
    var siteurl = "{{siteurl}}";
    var username = "{{username}}";
    var password = "{{password}}";
    var credential = new NetworkCredential(username, password);
    var context = new ClientContext(siteurl)
    {
        Credentials = new SharePointOnlineCredentials(credential.UserName, credential.SecurePassword)
    };
    context.Load(context.Web.Alerts, x => x.Include(y => y.ListID, y => y.ItemID));
    context.ExecuteQuery();
}

ところがこのコードを実行すると以下のエラーが発生することがあります。

Microsoft.SharePoint.Client.ServerException: 'ItemID is not available for this type of alert.'

リスト全体に設定する通知の場合、ItemID プロパティを参照しようとすると強制的にエラーになります。安全に読み込みたい場合は、それぞれの Alert について、AlertType プロパティを判断し、読み込むプロパティを変える必要があるということです。つまり、以下のようなコードになります。

private static void Main(string[] args)
{
    var siteurl = "{{siteurl}}";
    var username = "{{username}}";
    var password = "{{password}}";
    var credential = new NetworkCredential(username, password);
    var context = new ClientContext(siteurl)
    {
        Credentials = new SharePointOnlineCredentials(credential.UserName, credential.SecurePassword)
    };
    context.Load(context.Web.Alerts);
    context.ExecuteQuery();
    foreach (var alert in context.Web.Alerts)
    {
        switch (alert.AlertType) {
            case AlertType.List:
                context.Load(alert, x => x.ListID);
                break;
            case AlertType.Item:
                context.Load(alert, x => x.ListID, x => x.ItemID);
                break;
            default:
                context.Load(alert);
                break;
        }
        context.ExecuteQuery();
    }
}

AlertTime プロパティを Load に含めるとエラーになる

同様に、AlertTime プロパティも読み込むとエラーが発生することがあります。

private static void Main(string[] args)
{
    var siteurl = "{{siteurl}}";
    var username = "{{username}}";
    var password = "{{password}}";
    var credential = new NetworkCredential(username, password);
    var context = new ClientContext(siteurl)
    {
        Credentials = new SharePointOnlineCredentials(credential.UserName, credential.SecurePassword)
    };
    context.Load(context.Web.Alerts, x => x.Include(y => y.AlertTime));
    context.ExecuteQuery();
}

このコードは以下のエラーを発生させることがあります。

Microsoft.SharePoint.Client.ServerException: 'AlertTime property cannot be used on immediate alerts.'

コードは省略しますが、こちらの場合も AlertFrequency プロパティをみて Immediate かそうでないかで読み込みを変える必要があります。

まとめ

完全に使う側のこと考えないで設計しているよなとしか思えないです。もし使われる場合は十分注意してください。