使用 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 函式檢視中的 [取得函式 URL],然後複製 URL,以從 Azure 入口網站 取得函式 URL。

    取得函式 URL

    複製函式 URL

  2. 流覽至 Azure DevOps 中的專案,例如 https://dev.azure.com/<your organization>/<your project name>

  3. 從導覽功能表中,將滑鼠停留在齒輪,然後選取 [服務勾點]。

    從管理功能表選擇 [服務勾點]

  4. 如果這是您的第一個服務勾點,請選取 [+ 建立訂用帳戶]。

    從工具列選取 [建立新的訂用帳戶]

    如果您已設定其他服務勾點,請選取綠色加號 (+) 以建立新的服務勾點訂用帳戶。

    選取綠色加號以建立新的服務勾點訂用帳戶。

  5. 在 [新增服務攔截訂閱] 對話框中,從服務列表中選取 [Web Hooks ],然後選取 [ 下一步]。

    從服務清單中選取 Web 攔截

  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文字,並注意狀態會從 [工作進行中] 變更為 [準備檢閱]。

後續步驟

  • 在本文中,您已瞭解如何建立無伺服器 Azure 函式的基本概念,該函式會透過服務攔截接聽 PR 事件,並使用狀態 API 來張貼狀態消息。 如需提取要求狀態 API 的詳細資訊,請參閱 REST API 檔
  • 設定外部服務的分支原則