---
title: "ChocolateyパッケージのCI/CDをAppVeyorからGitHub Actionsに移行した記録 | C#もぐもぐ"
description: "さくらのクラウド関連ツールのChocolateyパッケージ3つについて、CI/CDをAppVeyorからGitHub Actionsに移行し、バージョン重複チェックやスケジュール実行などの自動化を改善した記録です。"
url: "https://blog.223n.tech/posts/235/"
---

# ChocolateyパッケージのCI/CDをAppVeyorからGitHub Actionsに移行した記録

2026年4月13日

[GitHub Actions](https://blog.223n.tech/tags/GitHub%20Actions/) [Chocolatey](https://blog.223n.tech/tags/Chocolatey/) [PowerShell](https://blog.223n.tech/tags/PowerShell/) [CI/CD](https://blog.223n.tech/tags/CI/CD/) [Program](https://blog.223n.tech/tags/Program/)

## はじめに

さくらのクラウド関連のCLIツールについて、
Chocolateyパッケージの自動ビルド・アップロードをAppVeyorで運用していました。

今回、これらのCI/CDをGitHub Actionsに移行し、あわせて処理の改善を行いました。

この記事では、3つのパッケージの移行作業と、GitHub Actionsで実現した自動化の改善点を記録として残します。

## 対象パッケージ

移行対象は、次の3つのChocolateyパッケージです。

| パッケージ名 | 対象ツール | 用途 |
| - | - | - |
| docker-machine-sakuracloud | Docker Machine driver | さくらのクラウド上にDockerホストを作成 |
| terraform-provider-sakuracloud | Terraform provider | さくらのクラウドのリソースをTerraformで管理 |
| usacloud | CLI client | さくらのクラウドの操作用CLIツール |

いずれも、上流リポジトリ（sacloud）の最新リリースを検知し、
Chocolateyパッケージとしてビルド・テスト・アップロードする仕組みです。

3つのパッケージはリポジトリ構成もワークフローもほぼ同じパターンのため、
共通部分をまとめて解説し、差分は個別に記載します。

## 移行の動機

AppVeyorで運用していた中で、いくつかの課題がありました。

- スケジュール実行の機能があったものの、AppVeyorに不慣れなため、手動でビルドをトリガー＆デプロイする必要があった
- バージョンの重複チェックが不十分で、Chocolateyの審査中パッケージを考慮できていなかった
- APIキーがAppVeyorの暗号化変数として埋め込まれており、管理しづらかった
- ソースコードとCI/CDの設定が別のサービスに分かれていた

GitHub Actionsに移行すれば、cron実行、GitHub Secretsによるキー管理、
ワークフローのコード管理がすべてリポジトリ内で完結します。

## リポジトリ構成

3つのリポジトリはすべて同じディレクトリ構成です。

```
repository/
├── .github/
│   └── workflows/
│       └── chocolatey.yml          # GitHub Actionsワークフロー
├── {package-name}/
│   ├── {package-name}.nuspec       # パッケージメタデータ（XML）
│   └── tools/
│       ├── chocolateyinstall.ps1   # インストールスクリプト
│       ├── chocolateyuninstall.ps1 # アンインストールスクリプト
│       └── LICENSE.txt
├── .gitignore
├── LICENSE
└── README.md
```

`.nuspec`とPowerShellスクリプトにはプレースホルダー（`__VERSION__`、`__HASH32__`、`__HASH64__`など）を埋め込んでおき、
ビルド時に実際の値へ置換します。

## AppVeyor時代のワークフロー

移行前のAppVeyorの設定は、次のような構成でした。

```yaml
# appveyor.yml（移行前）
version: '{build}'

branches:
  only:
    - master
    - /feature*/

environment:
  CHOCO_API_KEY:
    secure: 53zj0CRv8S/heDM1pmY/...（暗号化されたAPIキー）

build_script:
  - ps: |
      $release = Invoke-RestMethod -Uri "https://api.github.com/repos/sacloud/usacloud/releases/latest"
      $version = $release.tag_name -replace "^v", ""

      # 重複チェック（公開済みのみ）
      $chocoSearch = choco search usacloud --exact --limit-output
      # ... バイナリダウンロード → ハッシュ算出 → パッケージビルド

test_script:
  - ps: choco install usacloud -s .\ -f

deploy_script:
  - ps: choco push $nupkg --source https://push.chocolatey.org/ --api-key $env:CHOCO_API_KEY

artifacts:
  - path: '.\usacloud\*.nupkg'
```

この構成には、次の問題がありました。

- **スケジュール実行なし**
  - 新しいリリースを検知するには手動でビルドをトリガーする必要がある

- **審査中パッケージの考慮なし**
  - `choco search`は公開済みパッケージのみを返すため、審査中のパッケージと重複してアップロードしてしまう可能性がある

- **APIキーの管理**
  - AppVeyorの暗号化変数として`appveyor.yml`に直接埋め込まれている

- **エンコーディング問題**

  - `Out-File -Encoding utf8`で出力すると`BOM付きUTF-8`になる
  - しかし、これが`Windows PowerShell 5.1`で意図どおりに動く場合とそうでない場合がある

## GitHub Actionsのワークフロー

### トリガー設定

```yaml
name: Chocolatey パッケージの作成とアップロード

on:
  schedule:
    # 毎日 UTC 0:00 (JST 9:00) に実行
    - cron: "0 0 * * *"
  push:
    branches:
      - master
  workflow_dispatch:
    inputs:
      force:
        description: "既存バージョンのチェックをスキップして強制的にビルド・アップロードする"
        required: false
        type: boolean
        default: false

concurrency:
  group: chocolatey-publish
  cancel-in-progress: false
```

AppVeyorにはなかった3つのトリガーを設定しています。

- **schedule**: 毎日定時に実行し、上流の新リリースを自動検知
- **push**: masterブランチへのプッシュで即座に実行
- **workflow_dispatch**: 手動実行。`force`パラメーターで重複チェックをスキップ可能

`concurrency`設定で同時実行を防ぎ、パッケージの二重アップロードを回避しています。

### パイプライン全体像

ワークフローは8つのステップで構成されています。

1. Chocolateyのインストールまたはアップグレード
2. 上流リポジトリの最新バージョン取得
3. Chocolatey上のバージョン確認（審査中を含む）
4. バイナリのダウンロードとハッシュ算出
5. パッケージのビルド
6. パッケージのテスト
7. Chocolateyへプッシュ
8. 成果物のアップロード

ステップ3で最新バージョンが登録済みと判定された場合は、ステップ4以降はスキップされます。

### バージョン重複チェック（最大の改善点）

AppVeyor時代は`choco search`で公開済みパッケージのみを確認していました。

GitHub Actionsでは、ChocolateyのOData APIを使い、
審査中（moderation）のパッケージも含めて確認するようにしました。

```powershell
# OData API で審査中のパッケージも含めて全バージョンを取得
$apiUrl = "https://community.chocolatey.org/api/v2/FindPackagesById()?id='usacloud'"
$response = Invoke-RestMethod -Uri $apiUrl

if ($response) {
  $versions = $response | ForEach-Object {
    $_.properties.Version
  }

  $matched = $versions | Where-Object { $_ -like "$version.*" }
  if ($matched) {
    echo "skip=true" >> $env:GITHUB_OUTPUT
    return
  }
}
```

OData APIが失敗した場合は、`choco search`にフォールバックします。

```powershell
# フォールバック: choco search で公開済みバージョンのみ確認
$chocoSearch = choco search usacloud --exact --limit-output 2>$null
if ($chocoSearch) {
  $chocoVersion = ($chocoSearch -split '|')[1]
  if ($chocoVersion -like "$version.*") {
    echo "skip=true" >> $env:GITHUB_OUTPUT
    return
  }
}
```

さらに、`force`パラメーターが有効な場合は、
チェック自体をスキップして強制的にビルドを実行します。

この3段階のアプローチにより、審査中パッケージとの重複を防ぎつつ、
API障害時にもフォールバックでき、緊急時には強制実行も可能になりました。

### パッケージのビルド

ビルド処理では、プレースホルダーを実際の値に置換してから`choco pack`を実行します。

```powershell
$version = "${{ steps.version.outputs.version }}"
$packageVersion = "${version}.${{ github.run_number }}"

# .nuspec のバージョンを置換
(Get-Content '.\usacloud.nuspec' -Raw).Replace("__VERSION__", $packageVersion) |
  Out-File '.\usacloud.nuspec' -Encoding utf8NoBOM

# chocolateyinstall.ps1 のプレースホルダーを置換（BOM付きUTF-8で出力）
$content = (Get-Content '.\tools\chocolateyinstall.ps1' -Raw) `
  .Replace("__VERSION__", "v$version") `
  .Replace("__HASH32__", $hash32) `
  .Replace("__HASH64__", $hash64)
[System.IO.File]::WriteAllText(
  (Resolve-Path '.\tools\chocolateyinstall.ps1').Path,
  $content,
  [System.Text.UTF8Encoding]::new($true)
)

choco pack
```

パッケージバージョンは`{上流バージョン}.{run_number}`の形式です。

AppVeyor時代の`$env:APPVEYOR_BUILD_VERSION`に相当する部分を、
`${{ github.run_number }}`で代替しています。

### UTF-8 BOMエンコーディングの対応

移行作業でもっとも苦労したのが、PowerShellスクリプトの文字エンコーディングです。

Chocolateyは`chocolateyinstall.ps1`を`Windows PowerShell 5.1`で実行します。

`Windows PowerShell 5.1`は`BOM付きUTF-8`でないと日本語を正しく処理できない場合があるため、
`.ps1`ファイルはBOM付きUTF-8で出力する必要がありました。

```powershell
# BOM付きUTF-8で書き出し
[System.IO.File]::WriteAllText(
  $path,
  $content,
  [System.Text.UTF8Encoding]::new($true)  # $true = BOM付き
)
```

一方、`.nuspec`（XML）は`BOMなしUTF-8`で出力します。

そのため、ファイルの種類によって使い分けが必要でした。

この問題は、実際にChocolateyの審査でインストールテストが失敗して発覚し、
数回のコミットで修正を重ねました。

### テストとデプロイ

ビルドしたパッケージは、ローカルでインストール・アンインストールテストを行ったうえで、
Chocolateyにプッシュします。

```powershell
# テスト
choco install usacloud -s .\ -f
usacloud --version
choco uninstall usacloud

# デプロイ
$nupkg = Get-ChildItem .\usacloud\*.nupkg | Select-Object -First 1
choco push $nupkg.FullName --source https://push.chocolatey.org/ --api-key $env:CHOCO_API_KEY
```

APIキーはGitHub Secretsに格納しており、AppVeyorのように設定ファイルに暗号化キーを埋め込む必要がなくなりました。

## パッケージごとの差分

3つのパッケージの主な差分は、次の表のとおりです。

| 項目 | usacloud | terraform-provider | docker-machine |
| - | - | - | - |
| インストール先 | PATH上のディレクトリ | `%APPDATA%\terraform.d\plugins\` | PATH上のディレクトリ |
| 追加のプレースホルダー | なし | `__URL_32__`、`__URL_64__` | なし |
| アンインストール処理 | タブ補完の削除 | terraform.rcの整理 | 標準 |
| インストール後の検証 | `usacloud --version` | なし | なし |
| タブ補完の自動設定 | あり | なし | なし |

### usacloud固有: タブ補完の自動設定

usacloudのインストールスクリプトでは、PowerShellのタブ補完を自動で設定しています。

```powershell
# PowerShell タブ補完のセットアップ
$usacloudPath = Join-Path $toolsDir 'usacloud.exe'
$completionContent = & $usacloudPath completion powershell

if ($completionContent) {
  $completionContent | Out-File -FilePath $completionScriptPath -Encoding utf8 -Force
  # $PROFILE にドットソース行を追加
  Add-Content -Path $PROFILE -Value "`n. `"$completionScriptPath`""
}
```

失敗時はusacloud本体のインストールには影響しないよう、`try-catch`で囲んでいます。

### terraform-provider-sakuracloud固有: ダウンロードURLのテンプレート化

terraform-provider-sakuracloudは、リリースごとにダウンロードURLのパターンが異なるため、
URLもプレースホルダーとして管理しています。

```powershell
# ダウンロードURLもステップ出力として渡す
echo "url32=$url32" >> $env:GITHUB_OUTPUT
echo "url64=$url64" >> $env:GITHUB_OUTPUT
```

ビルドステップで`__URL_32__`と`__URL_64__`を置換することで、バージョンごとに正しいURLを設定しています。

## AppVeyorとGitHub Actionsの比較

移行前後の主な違いをまとめます。

| 項目 | AppVeyor | GitHub Actions |
| - | - | - |
| スケジュール実行 | なし | cron（毎日定時） |
| 手動実行 | 限定的 | workflow_dispatch + forceパラメーター |
| バージョン重複チェック | 公開済みのみ | 審査中を含む（OData API + フォールバック） |
| APIキー管理 | 暗号化変数（設定ファイル内） | GitHub Secrets |
| ビルド番号 | `$env:APPVEYOR_BUILD_VERSION` | `${{ github.run_number }}` |
| 成果物の保存 | artifacts セクション | upload-artifact v6 |
| エンコーディング | `Out-File -Encoding utf8` | `UTF8Encoding::new($true)`で明示的にBOM付き |
| 同時実行制御 | `max_jobs: 1` | concurrencyグループ |
| 設定の管理場所 | 別サービス | リポジトリ内（`.github/workflows/`） |

## 移行で得られた知見

### 審査中パッケージの確認は必須

Chocolateyにパッケージをプッシュすると、審査プロセスに入ります。

`choco search`では審査中のパッケージは表示されないため、OData APIを使って審査中のものも含めて確認する必要があります。

これを怠ると、同じバージョンのパッケージを重複してアップロードしてしまい、審査が失敗します。

### Windows PowerShell 5.1のエンコーディングに注意

GitHub Actionsのシェルとして`pwsh`（`PowerShell 7`）を使用していますが、
Chocolateyがインストールスクリプトを実行するのは`Windows PowerShell 5.1`です。

`PowerShell 7`では`Out-File -Encoding utf8`が`BOMなしUTF-8`を出力しますが、
`Windows PowerShell 5.1`では`BOM付きUTF-8`で入力されることを期待する場合があります。

このギャップにより日本語を含むスクリプトが文字化けするため、
`.ps1`ファイルは`[System.IO.File]::WriteAllText()`で`BOM付きUTF-8`を明示的に指定する必要がありました。

### フォールバック設計の重要性

外部APIに依存する処理には、フォールバックを設けておくべきです。

今回のバージョンチェックでは、`OData API` → `choco search` → `ビルド続行`という3段階の設計にしたことで、
APIの一時的な障害に影響されなくなりました。

## おわりに

3つのChocolateyパッケージのCI/CDをAppVeyorからGitHub Actionsに移行し、
次の改善を実現しました。

- 毎日の定時実行で上流の新リリースを自動検知
- 審査中パッケージを含めたバージョン重複チェック
- GitHub Secretsによるセキュアなキー管理
- 強制実行モードによる緊急時対応
- ソースコードとCI/CDの一元管理

3つのリポジトリで共通のワークフローパターンを使っているため、
1つのリポジトリで改善した内容を他のリポジトリにも展開しやすい構成になっています。

## 参考サイト

- sacloudの各種パッケージ

  - [sacloud/usacloud](https://github.com/sacloud/usacloud)
  - [sacloud/terraform-provider-sakuracloud](https://github.com/sacloud/terraform-provider-sakuracloud)
  - [sacloud/docker-machine-sakuracloud](https://github.com/sacloud/docker-machine-sakuracloud)

- GitHub Actionsのドキュメント
  - [GitHub Actions ワークフロー構文](https://docs.github.com/ja/actions/using-workflows/workflow-syntax-for-github-actions)

- Chocolateyのドキュメント

  - [Chocolatey パッケージ作成ドキュメント](https://docs.chocolatey.org/en-us/create/create-packages/)
  - [Chocolatey OData API](https://docs.chocolatey.org/en-us/community-repository/community-packages-api/)
  - [Chocolatey Install-ChocolateyZipPackage](https://docs.chocolatey.org/en-us/create/functions/install-chocolateyzippackage/)

[記事一覧に戻る](https://blog.223n.tech/posts/)
