Azure DevOps 提要获取最新版本的 nuget 包下载 url 或直接下载最新包

Azure DevOps feed get download url for a latest version of the nuget package or symply download latest package

我有 Azure DevOps 管道,它构建 nuget 包并将其部署到 Azure DevOps 源。在门户中,我可以找到下载 link 到特定版本的包。 如何找到 url 在提要中下载最新版本的 nuget 包?

或者如何下载最新版本的软件包? 理想情况下,只需通过 curl、dotnet 或任何在 dev windows 机器和一般 docker sdk 图像上反感的东西。 我倾向于走很远的路

如果您在导入 NuGet 程序包之前构建最新版本并将项目设置为最新版本,则可以使此自动化。这些都不是真正的管道的一部分,除非您正在订购步骤。

但是,我质疑 NuGetting 一个项目,然后将其包含在管道中,特别是如果这是一个更大的解决方案中的一个项目(具有依赖项)。如果您到了要部署到 NuGet 的地步,您应该更有意识地将有问题的项目移到另一个解决方案并使该解决方案产品化,而不是将其留在主解决方案中。然后,您将为新产品创建一个单独的管道,并像使用任何其他 NuGet 包一样使用它。现在,您可能会想到类似“但我在各种解决方案中使用它”这样的事情,所以这更容易,但实际上,这是一个更有说服力的理由将它分开并有意将其放在 NuGet 中而不是自动获取最新的(即,如果您是从另一家公司购买此程序集并且具有要求您在自动部署您的解决方案之前测试新版本的治理)。

如果您这样做是因为所讨论的项目仍在不断变化,那么我不会将消费者设置为自动获取最新的并使其成为有意的。即使您目前只有一个开发组,您也最好将转到 NuGet 的部分商品化。否则,真的没有必要构建 NuGet 包并使用(除非我缺少一些令人信服的理由不产品化并继续这种每次编译和版本控制的复杂方式)。

可能是 TL;DR?

How do I find url to download latest version of the nuget package in the feed?

请按照以下步骤找到此 url。

  1. 使用此 Rest API: Feed Management - Get Feeds 获取提要 id.
  2. 使用此 API: Artifact Details - Get Packages 获取有关提要中目标包的详细信息。 Url 看起来像:https://feeds.dev.azure.com/{organization}/{project}/_apis/packaging/Feeds/{feedId}/packages?packageNameQuery={packageName}&api-version=6.0-preview.1。从响应中,你会发现它的版本数组,最新版本用"isLatest":true标记,所以你在这个提要中得到这个包的最新版本。
  3. 此 Rest API:NuGet - Download Package 提供 url 在提要中下载最新版本的 nuget 包。

顺便说一句,请注意,如果在项目中创建提要,则必须提供上面 API 中的 project 参数。如果提要未与任何项目相关联,请从请求中省略项目参数。

对于发现@edward-han-msft 的答案有用(就像我一样)的任何人,这里有一个 python 3.6+ 脚本,用于从 Azure DevOps 工件(原文如此)提要下载所有包的所有版本.在我的示例中,我正在迁移到另一个 NPM 提要,因此该脚本还将下载的包发布到 .npmrc 中配置的任何 npm 注册表。根据您的需要进行调整。

import requests
from requests.auth import HTTPBasicAuth
import re
import json
import os
from os.path import exists
import subprocess

organization = '<Your organisation>'
project = '<Your project name>'
feed_name = '<The name of the artifact feed>'
feed_id = '<feedId - this can be found by examining the json response of the package feed>'

# Packages feed url
url_packages = f"https://feeds.dev.azure.com/{organization}/{project}/_apis/packaging/feeds/{feed_name}/packages?api-version=5.1-preview.1"

# ADO PAT
basic = HTTPBasicAuth("<I think this can be anything>", "<a DevOps PAT with Packaging Read scope>")

# fetch the packages feed and save locally
r1 = requests.get(url_packages, auth = basic)
open('packages.json', 'wb').write(r1.content) # for debug

# parse the json
packages = json.loads(r1.content)

for package in packages['value']:
    package_name = package['normalizedName']
    short_name = package_name.split('/')[1]
    versions_url = package['_links']['versions']['href']
    print(f'Package: {package_name} ({short_name})')

    # create a folder for each package
    package_folder = './'+short_name
    if not exists(package_folder):
        os.mkdir(package_folder)

    # fetch versions json
    r2 = requests.get(versions_url, auth = basic)
    versions = json.loads(r2.content)
    open(f'{package_folder}/versions.json', 'wb').write(r2.content) # for debug

    # This block iterates though the versions and discards ones that fall outside of semver e.g. x.x.x-canary or similar
    version_numbers = {}
    for package_version in versions['value']:
        # is it a release version? (semver compliant)
        version_number = package_version['normalizedVersion']
        match = re.search('^\d+.\d+.\d+$', version_number)
        if match:
            split = version_number.split('.')
            sortable_version = '%02d.%02d.%02d' % (int(split[0]),int(split[1]),int(split[2]))
            version_numbers[sortable_version] = version_number

    # the dictionary keys are a sortable format of the version e.g. 00.00.00
    version_numbers = sorted(version_numbers.items(), key = lambda kv: kv[0])
    print(version_numbers) # for debug

    for package_version in version_numbers:
        version_number = package_version[1] # package_version is a tuple
        package_filename = f'{package_folder}/{short_name}-{version_number}.tgz'

        # dowload package if not previously downloaded
        if not exists(package_filename):
            print(f'Downloading : {short_name}-{version_number}')
        
            package_url = f'https://pkgs.dev.azure.com/{organization}/{project}/_apis/packaging/feeds/{feed_id}/npm/packages/{package_name}/versions/{version_number}/content?api-version=6.0-preview.1'
            r3 = requests.get(package_url, allow_redirects=True, auth = basic)
            open(package_filename, 'wb').write(r3.content)
        
        # publish the package if not previsouly published
        if not exists(package_filename+'.published'):
            npm_publish = subprocess.run(["npm", "publish", package_filename])

            if npm_publish.returncode == 0:
                # create a file matching the package with .published extension to indicate successful publish
                subprocess.run(["touch", package_filename+'.published'])
            else:
                print(f'Error publishing {short_name}-{version_number}. Code: {npm_publish.returncode}')
    
    print('done.')

该脚本是幂等的,因为它保留下载包的副本,并且在成功调用 npm publish 后还会触及 <package name>.published 文件。