Azure DevOps Artifacts を本番運用し始めると、NuGet パッケージのバージョンやビュー操作で突然 PackageNotFoundException が出て詰まりがちです。この記事では、REST API と PowerShell を使ってパッケージを安全にプロモートしつつ、「ビューから外したい」ときに何ができて何ができないのかを、実務目線で整理します。
Azure DevOps Artifacts と NuGet ビュー運用の全体像
まずは今回の前提をざっくり整理します。
- パッケージは Azure Artifacts の フィード (feed) に保存される
- フィードには ビュー (view) があり、代表的には
@Local/@Prerelease/@Releaseが用意されている - ビューは「品質・公開範囲」を表現するフィルターのようなもので、同じフィード内で どのバージョンをどこまで見せるか を制御する
Azure DevOps 公式ドキュメントでも、「ビューを使って一部のパッケージだけを共有し、残りは組織内に留める」といった使い方が推奨されています。
ここに REST API と PowerShell を組み合わせて「自動プロモート」「古いバージョンだけビューから外す」のような運用を載せようとすると、今回のようなハマりどころにぶつかります。
症状整理:PackageNotFoundException と .nupkg が見つからないエラー
今回の典型的な症状は次のようなものです。
Invoke-RestMethod : {"$id":"1","innerException":null,
"message":"Cannot find the file {packagename}....nupkg
in package '{packagename} {versionid}' in feed '{feedid}'",
"typeKey":"PackageNotFoundException", ...}
やっていることを整理すると、だいたい次の流れになります。
feeds.dev.azure.comの REST API(Get Packages)でフィード内の NuGet パッケージとバージョン一覧を取得- 戻り値に含まれる
version.idをそのまま使い、pkgs.dev.azure.comのUpdate Package Versionエンドポイントに PATCH - すると
PackageNotFoundExceptionが発生する
URL の見た目はドキュメントと同じなのにエラーになるため、「フィード ID が違う?」「プロジェクト名が間違い?」と迷いがちですが、実は根本原因は パスパラメータに渡している “バージョン” の中身 にあります。
原因の核心:{packageVersion} には内部 ID ではなく「表示バージョン」を渡す
Azure DevOps の NuGet パッケージ更新 API は、次の形式の URL です。
PATCH https://pkgs.dev.azure.com/{organization}/{project}/_apis/packaging/feeds/{feedId}/nuget/packages/{packageName}/versions/{packageVersion}?api-version=7.1
ここで重要なのが {packageVersion} です。
feeds.dev.azure.com側のGet Packages / Get Package Versionsでは、各バージョンにid(GUID のような内部 ID)version(例:1.2.3や1.2.3-beta+buildのような「表示バージョン」)
Update Package Versionが要求しているのは URI パラメータとしての “version string” であり、内部 ID ではない
つまり、次のように version.id を使ってしまうと、サーバー側は {packagename}.{内部 ID}.nupkg のようなファイルを探しに行き、当然見つからず PackageNotFoundException になります。
# ❌ NG 例: version.id(内部 ID)を使っている
$patchUrl = "$pkgsBaseUrl/nuget/packages/$($thisPackage.name)/versions/$($version.id)?api-version=7.1"
Microsoft Q&A でも、まったく同じ症状に対して「packageVersion には version.version(表示バージョン)を渡す必要がある」と公式回答が出ています。
正しい PATCH URL の組み立て(PowerShell)
PowerShell での組み立ては次のようになります(URL エンコードを含めて記述)。
$pkgNameEnc = [System.Web.HttpUtility]::UrlEncode($thisPackage.name)
$pkgVersionEnc = [System.Web.HttpUtility]::UrlEncode($version.version) # ✅ version.version を使う
$patchUrl = "$pkgsBaseUrl/nuget/packages/$pkgNameEnc/versions/$pkgVersionEnc?api-version=7.1"
ポイントは次の 2 つです。
- フルネーム(
{packageName})ではなく IDベースの短い名前 を使っているか(Get Packagesのnameプロパティ)。 {packageVersion}にversion.version(文字列) を渡しているか
ここを修正するだけで、PackageNotFoundException 自体は解消できます。
feeds.dev.azure.com と pkgs.dev.azure.com の役割の違い
もう一つのつまづきポイントが、「同じフィードなのにドメインが 2 種類ある」ことです。
| ドメイン | 主な用途 | 代表的な API 例 |
|---|---|---|
feeds.dev.azure.com | フィード全体・パッケージ一覧などの「メタデータ」を取得 | GET .../_apis/packaging/feeds/{feedId}/packages(パッケージ一覧) |
pkgs.dev.azure.com | 実際のパッケージコンテンツのダウンロードや状態更新 | PATCH .../nuget/packages/{packageName}/versions/{packageVersion}(Update Package Version)DELETE .../nuget/packages/{packageName}/versions/{packageVersion}(Delete Package Version) |
実装の整理としては、次のように覚えておくと安全です。
- 一覧・検索・メタ情報の取得 →
feeds.dev.azure.com - PATCH / DELETE などの更新系 + コンテンツ配信 →
pkgs.dev.azure.com
また、フィードがプロジェクトスコープか組織スコープかによって、URL に /{project} を含めるかどうかが変わる点にも注意が必要です(いずれのドキュメントでも「フィードがプロジェクトに紐づく場合は project パラメータ必須」と明記されています)。
ビュー(View) の仕様:追加はできるが「外す」はサポートされていない
今回の質問の本丸は「特定バージョンをビューから外したい」でしたが、ここで一度仕様を整理します。
Microsoft の公式ドキュメントでは、ビューについて次のように説明されています。
- フィードはデフォルトで
@Local,@Prerelease,@Releaseの 3 つのビューを持つ - ビューは「どのバージョンをどの利用者に見せるか」を制御するフィルター
- パッケージは 上位ビューにプロモート(昇格) することはできるが、降格 (demotion) はサポートされない
特に最後の「降格はサポートしない」は重要で、ドキュメントの中では明確に
「Azure Artifacts ではパッケージのデモーションをサポートしていない。いったんプロモートしたパッケージは以前のビューに戻せない」
と書かれています。
実際、Microsoft Q&A の質問者が JSON Patch で op: "remove" を送ったところ、サーバーから
Operation 'Remove' is not supported on views.
というエラーが返ってきており、「remove はサポートされていない」ことが明らかになっています。
つまり、REST API レベルでも「ビューから外す(demote/remove)」という操作はできません。
ビューへの追加(プロモート)を REST API から行う正しい方法
では、ビューへの追加(プロモート)はどう書けばよいのでしょうか。ここは公式ドキュメントの例と若干クセがあるところなので、REST ボディの形を丁寧に見ておきます。
エンドポイント URL
NuGet の場合、ビューに追加する際も Update Package Version エンドポイントを使います。
# 組織スコープフィード
https://pkgs.dev.azure.com/{organization}/_apis/packaging/feeds/{feedId}/nuget/packages/{packageName}/versions/{packageVersion}?api-version=7.1
# プロジェクトスコープフィード
https://pkgs.dev.azure.com/{organization}/{project}/_apis/packaging/feeds/{feedId}/nuget/packages/{packageName}/versions/{packageVersion}?api-version=7.1
ここでも {packageVersion} は 表示バージョン文字列(例:1.2.3) である点に注意してください。
JSON ボディ(JSON Patch)
ドキュメントでは、「ビューへの追加には JSON Patch 文書を送る」と説明されています。
PowerShell での具体例は次のようになります(ドキュメントのサンプルをベースにしています)。
$body = @{
views = @{
op = "add"
path = "/views/-"
value = "Release" # 追加したいビューの『名前』を指定
}
} | ConvertTo-Json
Invoke-RestMethod -Uri $patchUrl -Method Patch -Headers $headers -Body $body
ここでのポイントは次のとおりです。
opにはaddを指定する(remove はサポートされない)pathは/views/-固定(views 配列の末尾に追加する意味)valueは ビュー名 を指定する- 例:
"Prerelease"/"Release" @Releaseのように@を付けたり、ビュー ID を渡したりしない
- 例:
ビュー名は Azure DevOps ポータルの Artifacts > Feed settings > Views で確認できます。
「ビューから外したい」ときに現実的に取れる選択肢
仕様上、ビューからの remove/demote はできないので、現実的な代替策を検討することになります。Azure Artifacts の削除ガイドでは、NuGet パッケージをフィードから「見えなくする」方法として次の 2 つが紹介されています。
- Unlist(非表示)
- Delete(削除してリサイクルビンへ)
選択肢 1: Unlist(非表示)で検索結果から外す
Unlist は「フィードや nuget.org の検索結果からバージョンを隠す」操作です。
- 利点:検索や通常の参照では見えなくなるため、利用者にとっては「なかったこと」にできる
- 欠点:完全削除ではないので、直接バージョンを指定すれば取得できる可能性がある
REST API では、Update Package Version の listed プロパティを false にすることで Unlist できます。
$body = @{
listed = $false
} | ConvertTo-Json
Invoke-RestMethod -Uri $patchUrl -Method Patch -Headers $headers -Body $body
これにより、GUI での検索や通常のパッケージ参照からは当該バージョンが除外されます。
選択肢 2: Delete(削除)でリサイクルビンへ移動
もうひとつの選択肢は、バージョンをフィードから削除してリサイクルビンに移動する方法です。
DELETE https://pkgs.dev.azure.com/{organization}/{project}/_apis/packaging/feeds/{feedId}/nuget/packages/{packageName}/versions/{packageVersion}?api-version=7.1
この API の説明には「パッケージバージョンをフィードからペアとなるリサイクルビンへ送る」とあり、実際にはすぐ完全削除されるのではなくリサイクルビンに移されます。
リサイクルビンからの復旧は別の API(Restore Package Version From Recycle Bin)で行えます。
| 方法 | ユーザーからの見え方 | 復旧可否 | 向いているケース |
|---|---|---|---|
| Unlist | 検索や一覧から消えるが、直接指定すれば参照されうる | いつでも再度 listed=true に戻せる | 「過去の事故バージョンを隠したい」が、履歴としては残しておきたい |
| Delete | フィードから完全に見えなくなる | リサイクルビンから復旧可能(一定期間) | 本番環境に残しておきたくないバージョン、容量節約、コンプライアンス対応 |
「ビューから外す」代わりに、「そのバージョンをそもそも見えなくしてしまう / 削除してしまう」という考え方に切り替えるのが、Azure Artifacts の前提に沿った運用になります。
PowerShell での実務的な実装例
ここからは、実際に PowerShell から REST API を叩く際のパターンをまとめます。
共通:REST 呼び出しの土台
$organization = "your-org"
$project = "your-project" # 組織スコープのフィードなら空でも OK
$feedId = "your-feed"
$packageName = "Your.Package"
$versionText = "1.2.3" # version.version を使う
$pat = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$pkgsBaseUrl = "https://pkgs.dev.azure.com/$organization/$project/_apis/packaging/feeds/$feedId"
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic " + `
[Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$pat"))
}
PAT は Azure DevOps のユーザー設定から作成し、スコープに Packaging > Read, write, & manage を付与しておきます。
例 1: 指定バージョンを Release ビューにプロモートする
$pkgNameEnc = [System.Web.HttpUtility]::UrlEncode($packageName)
$pkgVersionEnc = [System.Web.HttpUtility]::UrlEncode($versionText)
$uri = "$pkgsBaseUrl/nuget/packages/$pkgNameEnc/versions/$pkgVersionEnc?api-version=7.1"
$body = @{
views = @{
op = "add"
path = "/views/-"
value = "Release" # ビュー名(@ は不要)
}
} | ConvertTo-Json
Invoke-RestMethod -Uri $uri -Method Patch -Headers $headers -Body $body
これで、指定バージョンが @Release ビューに追加されます。
例 2: 指定バージョンを Unlist(一覧から非表示)にする
$pkgNameEnc = [System.Web.HttpUtility]::UrlEncode($packageName)
$pkgVersionEnc = [System.Web.HttpUtility]::UrlEncode($versionText)
$uri = "$pkgsBaseUrl/nuget/packages/$pkgNameEnc/versions/$pkgVersionEnc?api-version=7.1"
$body = @{
listed = $false # true に戻すと再表示
} | ConvertTo-Json
Invoke-RestMethod -Uri $uri -Method Patch -Headers $headers -Body $body
例 3: 指定バージョンを削除(リサイクルビンへ移動)する
$pkgNameEnc = [System.Web.HttpUtility]::UrlEncode($packageName)
$pkgVersionEnc = [System.Web.HttpUtility]::UrlEncode($versionText)
$uri = "$pkgsBaseUrl/nuget/packages/$pkgNameEnc/versions/$pkgVersionEnc?api-version=7.1"
Invoke-RestMethod -Uri $uri -Method Delete -Headers $headers
このあと、必要であれば Recycle Bin 用の API から復旧できます。
よくある落とし穴とチェックリスト
最後に、今回のようなハマりを防ぐためのチェックポイントをまとめます。
| チェック項目 | OK 条件 | NG のときの典型的な症状 |
|---|---|---|
| ドメインの使い分け | 取得系は feeds.dev.azure.com、更新系は pkgs.dev.azure.com を使っている | 404 / 401、あるいはパラメータ不足エラーなど |
{packageVersion} の中身 | version.version(1.2.3 等の文字列)を使用 | PackageNotFoundException(.nupkg ファイルが見つからない) |
| URL エンコード | packageName と packageVersion を URL エンコードしている(特に -, +, プレリリースタグを含む場合) | 一部のバージョンだけ 404 になる、プレリリースだけ失敗する |
| ビュー操作 | op="add" でビューに追加している。remove は使っていない | Operation 'Remove' is not supported on views. エラー |
| ビュー名の指定方法 | value = "Release" のように「名前」を指定(@Release や ID ではない) | レスポンス 200 なのにビューに追加されていない |
| PAT のスコープ | Packaging > Read, write, & manage を付与している | 403 Forbidden、あるいは一部の操作だけ失敗する |
| フィードスコープ | フィードがプロジェクトスコープなら URL に /{project} を含めている | 組織スコープでは動くがプロジェクトスコープだと 404 になる |
設計レベルでのベストプラクティス
Azure Artifacts のベストプラクティスでは、ビューとバージョニングを組み合わせた運用が推奨されています。
- SemVer の徹底:
MAJOR.MINOR.PATCH[-prerelease]形式に統一し、ビューと組み合わせて品質を表現する - ビューの役割をはっきり決める
@Local: 開発中+外部から取り込んだもの(既定)@Prerelease: テスト済みだが本番前@Release: 本番で利用可能な安定版
- プロモートは CI/CD パイプラインから自動で行う
- テストに通ったら
@Prereleaseに自動プロモート - 本番リリース完了時に
@Releaseへ自動プロモート
- テストに通ったら
- 誤ったバージョンの扱い
- 誤配布に気づいたらまず Unlist
- 本当に不要なら Delete → 必要に応じてリサイクルビンから復旧
- 「ビューから外す」という考え方ではなく、「見せない・残さない」で対応する
こうした運用ルールをチーム内で合意しておくことで、「誰かが手作業で Release に上げてしまった」「古い事故バージョンをどうするか」といったトラブルを減らせます。
まとめ:PackageNotFoundException とビュー操作で迷わないために
この記事で押さえたポイントを、最後にもう一度まとめます。
PackageNotFoundExceptionの主因は、{packageVersion}にversion.id(内部 ID)を渡していたこと- REST API では
version.version(表示バージョン文字列) をパスパラメータに使う feeds.dev.azure.comは一覧取得、pkgs.dev.azure.comは更新・コンテンツ配信と役割が分かれている- ビューは プロモート(add)専用 であり、demote/remove は仕様として非サポート
- 「ビューから外したい」場合は
- Unlist(listed=false)で検索結果から隠す
- Delete でリサイクルビンに移動し、必要なら復旧する
- PowerShell からは、
URL エンコード + version.version + views/op=addの組み合わせで安定したプロモート処理が書ける
Azure DevOps Artifacts の REST API は一見シンプルですが、「どの ID をどこで使うか」「ビューの思想はどうなっているか」を理解しておかないと今回のようにハマりがちです。仕様を踏まえて 「プロモートはできるが、降格はしない。その代わり Unlist/DELETE で制御する」 という前提に頭を切り替えておくと、今後の運用トラブルをかなり減らせるはずです。

コメント