Using GitLab API to create a DORA metrics dashboard

Alex O'Callaghan

使用 GitLab API 打造 DORA 指標儀表板

原文由 Alex O'Callaghan 發布,訂閱此部落格

衡量軟體開發團隊的生產力很困難。一旦開始用指標來衡量並以此評斷績效,就可能以帶來非預期影響的方式改變行為。依賴軟體交付過程的間接呈現,例如 Jira 這類專案管理工具,可能會誘使人們做出只讓指標變好看、卻無助於實際成果的行為——像是灌水故事點的估算,或是刻意挑選關閉票證的時機。

古德哈特定律

受到一篇探討大型科技公司如何追蹤生產力的 Pragmatic Engineer 文章啟發,我開始思考如何不再只仰賴從 Jira 流程衍生的指標來衡量生產力,而是回歸到我們實際變更與部署專案的真實情況。透過 GitLab API,我為 Mintel 的團隊建立了用來追蹤部署頻率與變更前置時間的儀表板。

該追蹤什麼?

我們目前用來衡量工程生產力的主要指標是週期時間(cycle time),也就是一張票證在 Jira 流程中從「In Progress」狀態流轉到「Done」所需的時間。由於所有工程團隊都使用 Jira,我們手邊本來就有這些資料,而 Jira 的報表工具也能產生各種報告來追蹤與視覺化這些數據。

不過,這些指標只是實際情況的再現,需要靠人工去與實際的程式碼變更與發布保持同步(而且還不一定會同步)。2020 年,DevOps 研究與評估(DORA)提出的四項關鍵指標來衡量軟體開發團隊的績效:

  • 部署頻率——組織成功發布至正式環境的頻率
  • 變更前置時間——一次提交進入正式環境所需的時間
  • 變更失敗率——造成正式環境故障的部署所占比例
  • 服務復原時間——組織從正式環境故障中復原所需的時間

部署頻率變更前置時間對我來說特別突出,因為這兩項指標相對容易從我們以程式碼為核心的工作流程中取得,也能作為生產力的參考指標。

部署頻率

要釐清部署頻率,第一步是找到可以用來判斷部署何時被觸發的依據。GitLab 本身就有部署功能,並整合了現成的 DORA 指標儀表板。看起來大功告成了!

可惜我們的專案並沒有使用這個部署功能。我希望能立刻取得有用的資料,而不是強迫團隊額外增加工作。

不過,我們多數專案都共用 GitLab CI 範本,採用的工作流程是在 main 分支上推送一個標籤來觸發部署。這些標籤都符合常見的 SemVer 格式(vX.X.X),而 GitLab 也提供了取得專案所有標籤的 API

我用 TypeScript 寫了一支簡單的腳本,來抓取團隊名下每個專案的標籤:

import { Gitlab } from "@gitbeaker/rest";

const api = new Gitlab({
  token: process.env.GITLAB_TOKEN,
});

const { id, name } = project;

const tags = await api.Tags.all(id);

// Filter to just release tags
const releaseTags = tags.filter((tag) => tag.name.match(/^v\d+\.\d+\.\d+$/));

我使用 gitbeaker 這個易用的 GitLab SDK 來發送 GitLab API 請求,接著用 csv-stringify 將發布紀錄寫入 CSV 檔案。

接著我把 CSV 資料匯入 Google 試算表,並產生了一些視覺化圖表:

每週部署次數圖表

這種做法沒有考量部署流水線執行所需的時間,也沒有判斷部署是否成功,但對我們來說,已經足以初步掌握部署頻率的概況。

變更前置時間

計算變更前置時間就稍微複雜一些。所謂變更前置時間,是指變更被提交到 main 分支後,到實際部署至正式環境所需的時間。在我們的專案中,所有變更都是透過合併請求合併到 main,而 GitLab 也有提供取得專案合併請求的 API

我對每一筆已合併的變更,是這樣計算其前置時間的:

Lead time = Time of release - Time of merge

這需要將每個合併請求與它所屬的發布版本關聯起來。我的做法是針對每個合併請求,找出該專案在 merged_at 時間點之後最早的一次發布:

import moment from "moment";

const { id, name } = project;

// Get all merged merge requests in descending time order
const merges = await api.MergeRequests.all({
  projectId: id,
  state: "merged",
  sort: "desc",
});
// Get all releases in ascending time order
const tags = await api.Tags.all(id, {
  orderBy: "updated",
  sort: "asc",
});
const releaseTags = tags.filter((tag) => tag.name.match(/^v\d+\.\d+\.\d+$/));

const leadTimes = [];

merges.forEach((merge) => {
  if (merge.merged_at) {
    // Find the next release
    const mergeTime = moment(merge.merged_at);
    const release = releaseTags.find((tag) => {
      return moment(tag.commit.committed_date).utc().isAfter(mergeTime);
    });
    if (release && release.commit.committed_date) {
      // Get the time (in hours) between merge and release
      const leadTime = moment
        .duration(moment(release.commit.comitted_date).utc().diff(mergeTime))
        .asHours();
      leadTimes.push({
        project: name,
        title: merge.title,
        merged_at: merge.merged_at,
        released_at: release.commit.committed_date,
        lead_time: leadTime,
      });
    }
  }
});

接著我再產生另一個 CSV 檔,記錄每一筆變更的前置時間,就能計算出一段時間內的平均變更前置時間。

定期自動更新的儀表板

我把這些資料分享給幾位工程經理,歷史資料中確實看到了一些有價值的洞察,但我們認為若能將這些指標納入衝刺回顧,並持續追蹤一段時間的變化,會更有價值。我不想每次都得手動執行腳本、再從 CSV 產生視覺化圖表,也希望能快速做出可用的東西,而不需要部署完整的服務與資料庫。

最後我決定用 ReactViteTypeScriptshadcn/ui 快速拼湊出一個儀表板。我把原本的腳本改成將 GitLab 回傳的 JSON 寫入 public 目錄下的檔案,這樣就能在建置時一次性抓取資料,再於儀表板前端計算指標。將變更與發布關聯並計算前置時間的過程比較耗時,所以我把這段邏輯保留在建置階段的腳本中。

接著我設定了排程 GitLab pipeline,每天自動拉取最新資料並重新建置網站,再透過 GitLab Pages 進行部署。

很快地,我就做出了一個能呈現這些指標、供團隊負責人使用的儀表板:

生產力指標儀表板

我們如何運用它與未來的改進方向

目前我們已經在 4 個不同的工程團隊中推行這個儀表板,並正在摸索如何讓它成為團隊自我檢視的有效工具。它已開始為團隊工作方式的討論、以及評估變更帶來的影響,提供了更客觀的依據。

我也希望它能成為與團隊外利害關係人溝通的另一項工具,用來展示開發者體驗的改善對生產力帶來的影響。

我們持續讓更多團隊導入這個儀表板,也更深入了解如何善用這些指標,以及是否還有其他能帶來洞察的指標。長期來看,這個客製化方案並不是那麼容易擴展或維護。如果證實它確實有價值,我們將會評估是否採用能提供類似儀表板的平台,例如 GitLab deployments、DXAtlassian Compass

可量化的數據也不是一切——我們也計畫在整個部門內進行開發者滿意度調查,更深入了解工程師對於現有工具與流程的主觀生產力感受。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言