Rust 和 GitHub Actions 的持續整合

語言: CN / TW / HK

關注「 Rust程式設計指北 」,一起學習 Rust,給未來投資

大家好,我是螃蟹哥。

CI/CD 是近些年熱門的話題,GitHub 上之前常用第三方工具實現,後來官方出了 GitHub Actions,所以有必要使用下。本文介紹一個 Rust 專案如何使用 GitHub Actions。

大約一個月前,我開始檢查我的計劃清單專案之一 — 學習 Rust 程式語言 [1]

與其他語言一樣,學習該語言的前期工作也是要掌握如何設定合適的開發環境。我用什麼 lint 工具、格式化等?更重要的是,我如何使用這些工具來建立合適的 CI/CD,以確保我 checkout 和部署的程式碼是正常的?就我而言,我的第一個 Rust 專案是更新一個內部工具,這個工具是我在 homelab 中用於通過 SSH 連線到機器: vaultssh [2] 。完成原型後,我試圖回答以下問題:

  • 哪些元件需要組成 CI 管道以確保我的程式碼正常?

  • 部署呢?

  • 我需要編寫自定義工具還是有可用的社群資源?

這是三部分系列中的第一篇文章,我們將在該系列文章中使用 Rust 和 GitHub Actions 來研究 CI/CD。在本文中,我們將研究建立 CI 管道。

GitHub Actions

針對此特定管道,我決定用 GitHub Actions [3] 。它正在成為 CI/CD 世界中的流行選擇,主要是因為許多專案已經在 GitHub 上,並且選擇它很自然。我一直是專案 Azure DevOps [4] 的長期使用者,並且很少對它感到失望,但是,我想如果我正在處理一種新語言,我也可以處理一個新的管道代理。

建立新儲存庫時,預設情況下會啟用 GitHub Actions。你可以通過單擊程式碼倉庫頂部的 “Actions” 選項卡來訪問它的主介面:

如果你之前從未將它用於你的倉庫,系統會提示你建立第一個工作流。可以通過將配置檔案新增到 .GitHub/workflows 倉庫內的路徑來新增工作流。GitHub Actions 將自動在此目錄中搜索 *.yml 檔案併為每個檔案新增一個新的工作流程。如副檔名所示,這些配置檔案是使用非常流行的 YAML [5] 語法建立的。

完全理解如何配置工作流超出了本系列的範圍。相反,我會向你指出 GitHub 網站上提供的 優秀文件 [6] 。在本系列的其餘部分,我將假設對 GitHub Actions 有基本的瞭解,但會在可能的情況下提供一些細節。

持續整合

Rust 一個很優秀的地方是它的包 (crate) 生態系統,特別是其明星雲集的包管理器,即 Cargo [7] 。這個工具不僅巧妙地包裝了對編譯器的呼叫,而且還處理了許多其他任務,包括一些非常適合持續整合的任務:格式化和測試程式碼。即使 Cargo 不檢查特定的 box,它也有豐富的外掛生態系統來實現出色的開發體驗(我們將使用的)。

Rust 中的基本 CI 工作流依賴以下子命令:

$> cargo fmt   # Lint
$> cargo test  # Test

請注意,這不是詳盡的檢查,可以而且應該考慮許多其他工具(即程式碼覆蓋率)。但是,在本文我們將使用幾個簡單。包括這個:

$> cargo clippy

這個 clippy 工具添加了一組額外的 lints,在 cargo check 這些lints 之上可以捕獲許多常見的 Rust 陷阱。它不是 Cargo 開箱即用的,但是,它是我之前談到的豐富外掛生態系統的一部分,可以使用 rustup 以下命令進行安裝:

$> rustup component add clippy

上面的內容很重要,因為我們需要在 CI 工作流程中考慮到這一點。

記住這四個命令後,我們已準備好在我們的工作流程中建立第一個作業。但是,此時,我們需要停下來問問自己列表中的第三個問題,我是否需要在這裡編寫自定義工具?在這種情況下,它不會寫太多,即只需確保 Rust 在 runner 上正確設定,然後呼叫並捕獲 Cargo 的輸出。然而,當我學習 GitHub Actions 時,我不斷地想起這個神奇的 GitHub Marketplace [8] ,它應該充滿了有用的工具。它確實沒有讓人失望,因為不久我就遇到了出色的 actions-rs [9] 工具集。它巧妙地包裝了我們執行上述檢查所需的所有功能,甚至處理確保在執行器上正確設定 Rust 環境。現在我們可以建立我們的第一個作業:

  lints:
    name: Run cargo fmt and cargo clippy
    runs-on: ubuntu-latest
    steps:
      - name: Checkout sources
        uses: actions/[email protected]
      - name: Install toolchain
        uses: actions-rs/[email protected]
        with:
          profile: minimal
          toolchain: stable
          override: true
          components: rustfmt, clippy
      - name: Run cargo fmt
        uses: actions-rs/[email protected]
        with:
          command: fmt
          args: --all -- --check
      - name: Run cargo clippy
        uses: actions-rs/[email protected]
        with:
          command: clippy
          args: -- -D warnings

這項作業負責執行 cargo fmtcargo clippy 。我們在 Ubuntu 系統上執行,並利用 actions-rs 提供的兩個操作。該 toolchain action 負責用於 rustup ,確保在執行器上正確設定 Rust 環境。此時,我們使用四個引數:

  • profile :確定 rustup 執行時安裝的內容。我們將其設定為 minimum 以安裝編譯大多數 Rust 應用程式所需的最低限度。這意味著每次我們的 CI 執行時,我們都需要設定這個環境——所以我們能做的任何事情來減少它所花費的時間都是成功的。 有關更多資訊,請參閱文件 [10]
  • toolchain : 大致可以翻譯成 Rust 編譯器的版本安裝。這可以是特定版本或釋出渠道(channel)。在我們的例子中,我們使用的是穩定的 Rust,因為我的特定專案就是用它來構建的。值得注意的是,一些 Rust 專案針對多個版本的 Rust 編譯器進行測試,在這種情況下,使用 矩陣策略 [11] 是一個很好的方式。
  • override :確保 toolchain 上面指定的內容用於我們剩餘的工作。雖然在這種情況下不是絕對必要的,因為我們只安裝了一個工具鏈,但最好讓它明確說明我們正在使用什麼。
  • components
    cargo fmt
    cargo clippy
    rustup
    

第三個和第四個 action 呼叫 Cargo 並允許我們傳遞任意引數和選項。像我一樣,你可能會問為什麼我們不直接使用 cargo run 命令令進行呼叫。雖然跨操作保持一致(使用相同的提供者)有一定的價值,但在這種情況下,更大的好處是 actions-rs 操作完成了捕獲 Cargo 輸出並將其合併到 GitHub Actions UI 中可理解的檢視的工作。此外,正如我們稍後將看到的,它提供了一個漂亮的開關,供 cross 在進行交叉編譯時使用。

剩下的工作將模仿我們上面所做的大部分工作,只將引數更改為 Cargo

  test:
    name: Run cargo test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout sources
        uses: actions/[email protected]
      - name: Install toolchain
        uses: actions-rs/[email protected]
        with:
          profile: minimal
          toolchain: stable
          override: true
      - name: Run cargo test -no-run
        uses: actions-rs/[email protected]
        with:
          command: test --no-run
      - name: Run cargo test
        uses: actions-rs/[email protected]
        with:
          command: test

感謝 /u/matklad [12] 的建議,我將測試分為兩部分。第一個呼叫 cargo testno-run 程式碼編譯的一切,但沒有實際執行測試。第二個實際運行了測試。這很有用,因為它使我們能夠很好地瞭解編譯時間,因為 GitHub Actions 會自動報告每個步驟所花費的時間。

Caching

在結束之前,還有最後一件事值得解決。那些使用過 Rust 的人對這樣一個事實並不陌生,即編譯器對於大中型專案通常很慢(具有正確的依賴關係,甚至可以擴充套件到小型專案)。這通常通過使用本地快取來加速,這會顯著增加第一個之後的後續構建。但是,如前所述,執行器在每次作業執行後都會被擦除,因此我們不能依賴於我們的工作流執行時的情況。

幸運的是 GitHub Actions 提供了一個操作來幫助我們:

- name: Load cache
  uses: actions/[email protected]
  with:
    path: |
      ~/.cargo/registry
      ~/.cargo/git
      target
    key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

此操作需要一個以換行符結尾的路徑列表,這些路徑將被備份到快取中以在將來的執行中重新使用。快取由我們使用執行器作業系統構建的唯一鍵和 Cargo 鎖定檔案的雜湊值標識,以確保我們僅在適當的情況下使用它。執行器作業系統在這裡似乎沒有必要,但稍後當我們檢視部署時它變得很重要,因此我將其包括在內以保持一致性。

值得注意的是,這可以通過在將 target 目錄上傳到快取之前清除目錄來進一步改進。 rust-analyzer [13] 專案有一個很好的例子,你也可以使用另一個 GitHub 上的操作:

- name: Cache
  uses: Swatinem/[email protected]

到目前為止,應將上述內容附加到我們所有的作業中,以最大限度地利用它。在我自己的專案中,我發現在引入快取後,我的 CI 作業執行時間減少了 30% 以上。

結論

最終的工作流程如下所示:

on:
  push:
    paths-ignore:
      - '**.md'
  pull_request:
    paths-ignore:
      - '**.md'
  workflow_dispatch:

name: CI

env:
  RUST_TOOLCHAIN: stable
  TOOLCHAIN_PROFILE: minimal

jobs:
  lints:
    name: Run cargo fmt and cargo clippy
    runs-on: ubuntu-latest
    steps:
      - name: Checkout sources
        uses: actions/[email protected]
      - name: Install toolchain
        uses: actions-rs/[email protected]
        with:
          profile: ${{ env.TOOLCHAIN_PROFILE }}
          toolchain: ${{ env.RUST_TOOLCHAIN }}
          override: true
          components: rustfmt, clippy
      - name: Cache
       uses: Swatinem/[email protected]
      - name: Run cargo fmt
        uses: actions-rs/[email protected]
        with:
          command: fmt
          args: --all -- --check
      - name: Run cargo clippy
        uses: actions-rs/[email protected]
        with:
          command: clippy
          args: -- -D warnings
  test:
    name: Run cargo test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout sources
        uses: actions/[email protected]
      - name: Install toolchain
        uses: actions-rs/[email protected]
        with:
          profile: ${{ env.TOOLCHAIN_PROFILE }}
          toolchain: ${{ env.RUST_TOOLCHAIN }}
          override: true
      - name: Cache
       uses: Swatinem/[email protected]
      - name: Run cargo test --no-run
        uses: actions-rs/[email protected]
        with:
          command: test --no-run
      - name: Run cargo test
        uses: actions-rs/[email protected]
        env:
          RUST_TEST_THREADS: 1
        with:
          command: test

我已將 rustup 配置檔案和工具鏈引數重構為環境變數,以使其更加明確我們正在使用的內容。工作流配置為在每個推送和拉取請求上執行,不包括任何僅修改 Markdown 檔案的提交。此外,你可以在你的自述檔案中新增一個 badge,它會報告你最新的工作流程執行的狀態:

<a href="https://GitHub.com/jmgilman/vaultssh/actions/workflows/ci.yml">
  <img src="https://GitHub.com/jmgilman/vaultssh/actions/workflows/ci.yml/badge.svg"/>
</a>

替換倉庫名稱並確保你的工作流被呼叫 ci.yml

下篇文章我們將研究向工作流程新增持續部署策略。

作者:HomeOps,原文連結:https://www.homeops.dev/continuous-integration-with-github-actions-and-rust/,螃蟹哥編譯。

參考資料

[1]

Rust 程式語言: https://www.rust-lang.org/

[2]

vaultssh: https://GitHub.com/jmgilman/vaultssh

[3]

GitHub Actions: https://GitHub.com/features/actions

[4]

Azure DevOps: https://azure.microsoft.com/en-us/services/devops/

[5]

YAML: https://yaml.org/

[6]

優秀文件: https://docs.GitHub.com/en/actions

[7]

Cargo: https://GitHub.com/rust-lang/cargo

[8]

GitHub Marketplace: https://GitHub.com/marketplace?type=actions

[9]

actions-rs: https://GitHub.com/actions-rs

[10]

有關更多資訊,請參閱文件: https://rust-lang.GitHub.io/rustup/concepts/profiles.html

[11]

矩陣策略: https://docs.GitHub.com/en/actions/reference/workflow-syntax-for-GitHub-actions#jobsjob_idstrategymatrix

[12]

/u/matklad: https://www.reddit.com/user/matklad/

[13]

rust-analyzer: https://GitHub.com/rust-analyzer/rust-analyzer/blob/94d9fc2a28ea5d97e3a9293b9dac05bdb00304cc/xtask/src/pre_cache.rs#L30-L53

推薦閱讀

覺得不錯,點個贊吧

掃碼關注「 Rust程式設計指北