Azure DevOps ArtifactsでのNuGet PackageNotFoundException対処とビュー運用ベストプラクティス

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", ...}

やっていることを整理すると、だいたい次の流れになります。

  1. feeds.dev.azure.com の REST API(Get Packages)でフィード内の NuGet パッケージとバージョン一覧を取得
  2. 戻り値に含まれる version.id をそのまま使い、pkgs.dev.azure.com の Update Package Version エンドポイントに PATCH
  3. すると 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 つが紹介されています。

  1. Unlist(非表示)
  2. 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 で制御する」 という前提に頭を切り替えておくと、今後の運用トラブルをかなり減らせるはずです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次