使用 Azure Functions 创建自定义分支策略

Azure DevOps Services | Azure DevOps Server 2022 - Azure DevOps Server 2019

拉取请求 (PR) 工作流为开发人员提供了从同行以及自动化工具获取代码反馈的机会。 第三方工具和服务可以使用 PR 状态 API 参与 PR 工作流。 本文将指导你完成使用 Azure Functions 创建自定义分支策略的过程,以验证 Azure DevOps Services Git 存储库中的 PR。 使用 Azure Functions,无需担心预配和维护服务器,尤其是在工作负载增加时。 Azure Functions 提供了高度可靠和安全的完全托管计算平台。

有关 PR 状态的详细信息,请参阅使用拉取请求状态自定义和扩展拉取请求工作流

先决条件

Azure DevOps 中具有 Git 存储库的组织。 如果没有组织,请注册以在无限制的免费专用 Git 存储库中上传和共享代码。

创建基本的 Azure 函数以侦听 Azure Repos 事件

按照创建第一个 Azure 函数文档创建一个简单的函数。 将示例中的代码修改为如下所示:

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    try
    {
        log.Info("Service Hook Received.");

        // Get request body
        dynamic data = await req.Content.ReadAsAsync<object>();

        log.Info("Data Received: " + data.ToString());

        // Get the pull request object from the service hooks payload
        dynamic jObject = JsonConvert.DeserializeObject(data.ToString());

        // Get the pull request id
        int pullRequestId;
        if (!Int32.TryParse(jObject.resource.pullRequestId.ToString(), out pullRequestId))
        {
            log.Info("Failed to parse the pull request id from the service hooks payload.");
        };

        // Get the pull request title
        string pullRequestTitle = jObject.resource.title;

        log.Info("Service Hook Received for PR: " + pullRequestId + " " + pullRequestTitle);

        return req.CreateResponse(HttpStatusCode.OK);
    }
    catch (Exception ex)
    {
        log.Info(ex.ToString());
        return req.CreateResponse(HttpStatusCode.InternalServerError);
    }
}

为 PR 事件配置服务挂钩

服务挂钩是 Azure DevOps Services 的一项功能,可在发生某些事件时向外部服务发出警报。 对于此示例,需要为 PR 事件设置服务挂钩,当拉取请求发生更改时,Azure 函数将收到通知。 若要在拉取请求发生更改时接收 POST 请求,需要为服务挂钩提供 Azure 函数 URL。

对于此示例,需要配置 2 个服务挂钩。 第一个用于“已创建拉取请求”事件,第二个用于“已更新拉取请求”事件。

  1. 单击 Azure 函数视图中的“Get 函数 URL”从 Azure 门户中获取函数 URL,然后复制该 URL。

    Get 函数 URL

    Copy 函数 URL

  2. 浏览到 Azure DevOps 中的项目,例如 https://dev.azure.com/<your organization>/<your project name>

  3. 在导航菜单中,将鼠标悬停在齿轮上,然后选择“服务挂钩”。

    从管理菜单中选择“服务挂钩”

  4. 如果这是第一个服务挂钩,请选择“+ 创建订阅”。

    从工具栏中选择“创建新订阅”

    如果已配置其他服务挂钩,请选择绿色加号 (+) 以创建新的服务挂钩订阅。

    选择绿色加号以创建新的服务挂钩订阅。

  5. 在“新建服务挂钩订阅”对话框中,从服务列表中选择“Web 挂钩”,然后选择“下一步”。

    从服务列表中选择 Webhook

  6. 从事件触发器列表中选择“已创建拉取请求”,然后选择“下一步”。

    从事件触发器列表中选择“已创建拉取请求”

  7. 在“操作”页的“URL”框中,输入在步骤 1 中复制的 URL。 选择“测试”,向服务器发送测试事件。

    输入 URL 并选择“测试”以测试服务挂钩

    在 Azure 函数日志窗口中,你将看到一个返回 200 OK 的传入 POST,指示函数收到了服务挂钩事件。

    HTTP Requests
    -------------
    
    POST /                         200 OK
    

    在“测试通知”窗口中,选择“响应”选项卡以查看来自服务器的响应详细信息。 你应会看到来自服务器的响应。

    选择“响应”选项卡以查看测试结果

  8. 关闭“测试通知”窗口,然后选择“完成”以创建服务挂钩。

再次完成步骤 2-8,但这次是配置“已更新拉取请求”事件。

重要

请务必执行上述步骤两次,为“已创建拉取请求”事件和“已更新拉取请求”事件创建服务挂钩。

创建拉取请求以验证 Azure 函数是否收到通知。

将状态发布到 PR

现在,服务器可以在创建新 PR 时接收服务挂钩事件,请将其更新,以便将状态回发 PR。 可以使用服务挂钩发布的 JSON 有效负载来确定要在 PR 上设置的状态。

更新 Azure 函数的代码,如以下示例所示。

请确保使用组织名称、项目名称、存储库名称和 PAT 令牌来更新代码。 为了获得更改 PR 状态的权限,PAT 需要 vso.code_status 范围,可以通过在“创建个人访问令牌”页上选择“代码(状态)”范围来授予该范围。

重要

此示例代码将 PAT 存储在代码中以简化示例。 建议将机密存储在 KeyVault 中,并从那里检索它们。

此示例检查 PR 标题,以查看用户是否向标题添加了 WIP 来指示 PR 是否正在进行中。 如果是这样,示例代码将更改发布回 PR 的状态。 将 Azure 函数中的代码替换为以下代码,以更新发布回 PR 的状态。

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;

private static string organizationName = "[Organization Name]";  // Organization name
private static string projectName      = "[Project Name]";       // Project name
private static string repositoryName   = "[Repo Name]";          // Repository name

/*
    This is here just to simplify the sample, it is recommended to store
    secrets in KeyVault and retrieve them from there.
*/
private static string pat = "[PAT TOKEN]";

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    try
    {
        log.Info("Service Hook Received.");

        // Get request body
        dynamic data = await req.Content.ReadAsAsync<object>();

        log.Info("Data Received: " + data.ToString());

        // Get the pull request object from the service hooks payload
        dynamic jObject = JsonConvert.DeserializeObject(data.ToString());

        // Get the pull request id
        int pullRequestId;
        if (!Int32.TryParse(jObject.resource.pullRequestId.ToString(), out pullRequestId))
        {
            log.Info("Failed to parse the pull request id from the service hooks payload.");
        };

        // Get the pull request title
        string pullRequestTitle = jObject.resource.title;

        log.Info("Service Hook Received for PR: " + pullRequestId + " " + pullRequestTitle);

        PostStatusOnPullRequest(pullRequestId, ComputeStatus(pullRequestTitle));

        return req.CreateResponse(HttpStatusCode.OK);
    }
    catch (Exception ex)
    {
        log.Info(ex.ToString());
        return req.CreateResponse(HttpStatusCode.InternalServerError);
    }
}

private static void PostStatusOnPullRequest(int pullRequestId, string status)
{
    string Url = string.Format(
        @"https://dev.azure.com/{0}/{1}/_apis/git/repositories/{2}/pullrequests/{3}/statuses?api-version=4.1",
        organizationName,
        projectName,
        repositoryName,
        pullRequestId);

    using (HttpClient client = new HttpClient())
    {
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(
                ASCIIEncoding.ASCII.GetBytes(
                string.Format("{0}:{1}", "", pat))));

        var method = new HttpMethod("POST");
        var request = new HttpRequestMessage(method, Url)
        {
            Content = new StringContent(status, Encoding.UTF8, "application/json")
        };

        using (HttpResponseMessage response = client.SendAsync(request).Result)
        {
            response.EnsureSuccessStatusCode();
        }
    }
}

private static string ComputeStatus(string pullRequestTitle)
{
    string state = "succeeded";
    string description = "Ready for review";

    if (pullRequestTitle.ToLower().Contains("wip"))
    {
        state = "pending";
        description = "Work in progress";
    }

    return JsonConvert.SerializeObject(
        new
        {
            State = state,
            Description = description,
            TargetUrl = "https://visualstudio.microsoft.com",

            Context = new
            {
                Name = "PullRequest-WIT-App",
                Genre = "pr-azure-function-ci"
            }
        });
}

创建新的 PR 以测试状态服务器

现在服务器正在运行并侦听服务挂钩通知,请创建一个拉取请求来测试它。

  1. 从文件视图开始。 编辑存储库中的 readme.md 文件(如果没有 readme.md,则编辑任何其他文件)。

    从上下文菜单中选择“编辑”

  2. 进行编辑并将更改提交到存储库。

    编辑文件,然后在工具栏中选择“提交”

  3. 请务必将更改提交到新分支,以便在下一步中创建 PR。

    输入新的分支名称,然后选择“提交”

  4. 选择“创建拉取请求”链接。

    在建议栏中选择“创建拉取请求”

  5. 在标题中添加 WIP 以测试应用的功能。 选择“创建”以创建 PR。

    将 WIP 添加到默认 PR 标题

  6. 创建 PR 后,会看到状态部分,其中“正在进行”条目链接到有效负载中指定的 URL。

    包含“正在进行”条目的状态部分。

  7. 更新 PR 标题并删除 WIP 文本,注意状态将从“正在进行”更改为“可供评审”。

后续步骤

  • 本文介绍了如何通过服务挂钩创建侦听 PR 事件的无服务器 Azure 函数以及如何使用状态 API 发布状态消息的基础知识。 有关拉取请求状态 API 的详细信息,请参阅 REST API 文档
  • 为外部服务配置分支策略