CI/CD 十關

LV.3/10

Artifacts

把建置產物交給下一個 job,並在 GitLab 介面看到測試報告

基礎約 20 分鐘artifacts、dependencies、reports:junit

學習目標

  • 知道每個 job 都是全新的容器,檔案不會自動留到下一個 job。
  • 會用 artifacts:paths 保存檔案、expire_in 設保存期限。
  • 會用 dependencies 控制要下載哪些產物。
  • 會用 reports:junit 讓測試結果顯示在 GitLab 介面。

觀念

build ──(dist/ 上傳到 GitLab)──▶ test 下載 dist/ ──▶ 產生 report.xml
                                                   └▶ Tests 分頁
report:dependencies: [],不下載任何東西
設定說明
artifacts:paths要保存的檔案或資料夾,路徑相對於專案根目錄
artifacts:expire_in保存多久,例如 1 week;不寫就用執行個體的預設值
artifacts:whenon_success(預設)、on_failure、always
artifacts:reports:junit測試報告,會出現在 pipeline 與 MR 上
dependencies不寫=下載前面所有階段的產物;[]=都不要

artifacts 與 cache 的差別

artifactscache(Lv.5)
用途傳遞結果:建置產物、報告加速過程:套件下載
可靠性一定拿得到盡力而為,可能沒命中
可下載可以不行

動手做

  1. push 後到 build job 頁面點「Browse」,看得到 dist/index.html。
  2. 打開 pipeline 的「Tests」分頁,會看到 2 項測試通過。
  3. 把 build.py 裡的 <h1> 拿掉,test 會失敗,報告裡看得到失敗原因。

常見錯誤

  • 產物路徑寫錯,job 成功但紀錄出現 No files to upload。
  • 把 node_modules 這類大資料夾當 artifacts:應該用 cache。

下一關

Lv.4 學變數:內建變數、自訂變數、在 job 之間傳值。

範例檔案

資料夾 lv03-artifacts/ 的全部檔案。.gitlab-ci.yml 可以直接複製到自己的專案根目錄執行。

lv03-artifacts/.gitlab-ci.yml
lv03-artifacts/.gitlab-ci.yml
# Lv.3 Artifacts:把 job 產生的檔案保存下來,交給後面的 job 或讓人下載
# 每個 job 都在全新的容器裡執行,檔案不會自己留下來,要靠 artifacts 傳遞。

stages:
  - build
  - test
  - report

default:
  image: python:3.13-alpine

build:
  stage: build
  script:
    - python3 lv03-artifacts/build.py
  artifacts:
    name: "lv03-site-$CI_COMMIT_SHORT_SHA"
    paths:
      - lv03-artifacts/dist/
    expire_in: 1 week          # 到期自動刪除,省空間

test:
  stage: test
  # dependencies:只下載指定 job 的產物。不寫=下載前面所有階段的產物
  dependencies: [build]
  script:
    - python3 lv03-artifacts/test_build.py
  artifacts:
    when: always               # 測試失敗也要保存報告
    paths:
      - lv03-artifacts/report.xml
    reports:
      junit: lv03-artifacts/report.xml   # 在 pipeline 的 Tests 分頁顯示結果
    expire_in: 1 week

report:
  stage: report
  dependencies: []             # 空陣列=不下載任何產物,job 啟動更快
  script:
    - test ! -d lv03-artifacts/dist && echo "dependencies 是空陣列,所以這裡沒有 dist/"
    - echo "要下載產物:pipeline 頁面右側的下載按鈕,或 job 頁面的 Browse"
lv03-artifacts/build.py
lv03-artifacts/build.py
"""Lv.3 建置腳本:產生一個小網站到 dist/,模擬真實專案的建置產物。"""
import os
from pathlib import Path

DIST = Path(__file__).parent / 'dist'
DIST.mkdir(exist_ok=True)
sha = os.environ.get('CI_COMMIT_SHORT_SHA', 'local')
(DIST / 'index.html').write_text(
    '<!doctype html><html lang="zh-Hant"><meta charset="utf-8">'
    f'<title>Lv.3 產物</title><h1>Lv.3 artifacts 範例</h1><p>版本 {sha}</p></html>\n',
    encoding='utf-8')
(DIST / 'version.txt').write_text(sha + '\n', encoding='utf-8')
print('已產生:', ', '.join(sorted(p.name for p in DIST.iterdir())))
lv03-artifacts/test_build.py
lv03-artifacts/test_build.py
"""Lv.3 測試:檢查 build 產物,並輸出 JUnit XML 讓 GitLab 顯示測試結果(只用標準函式庫)。"""
from pathlib import Path
import sys
import xml.etree.ElementTree as ET

HERE = Path(__file__).parent
DIST = HERE / 'dist'


def check_index():
    html = (DIST / 'index.html').read_text(encoding='utf-8')
    assert '<h1>' in html, '首頁沒有標題'


def check_version():
    assert (DIST / 'version.txt').read_text(encoding='utf-8').strip(), 'version.txt 是空的'


cases = [check_index, check_version]
suite = ET.Element('testsuite', name='lv03-build', tests=str(len(cases)))
failed = 0
for case in cases:
    node = ET.SubElement(suite, 'testcase', classname='lv03', name=case.__name__)
    try:
        case()
        print('通過', case.__name__)
    except (AssertionError, OSError) as err:
        failed += 1
        ET.SubElement(node, 'failure', message=str(err))
        print('失敗', case.__name__, err)
suite.set('failures', str(failed))
ET.ElementTree(suite).write(HERE / 'report.xml', encoding='utf-8', xml_declaration=True)
sys.exit(1 if failed else 0)