Skip to content

第 27 天|本機測試都通過了,放到 GitHub Actions 卻失敗 ​

前言 ​

第 25 天,我們遇過測試全綠,卻把漏洞當成正確答案的情況。因此,昨天替購物車寫測試時,我們先確認該驗什麼,再對照兩個版本:乾淨版四支都通過,含缺陷版則抓到兩筆金額錯誤。

測試有了,接下來只要記得跑,就能持續檢查。問題是,趕著交付時,「記得跑」往往最先被忘記。

今天就把同一套測試交給 GitHub Actions,讓符合條件的程式更新自動觸發檢查。跑完也要留下報告和證據,其他同事才知道發生了什麼。

當時第一次搬上去,連乾淨版都失敗了。不過,我們今天先做到「流程跑起來、結果分得清、證據拿得回來」,原因留到明天查。否則只看到紅燈就開始改,最後很可能修到最無辜的那一行。

開始之前 ​

先打開昨天使用的配套專案 sdet-skills/。今天接的是 tests/e2e/ 裡那四支測試,設定檔仍是 tests/playwright.config.ts。tests/broken/ 和 tests/flaky/ 是另外的練習,不要一起放進來。

這裡沿用第 26 天的公開站測試,並不是把第 25 天本機修復權限問題的那套測試搬上來。兩個案例分開看:那次教我們確認測試的答案,這次練習讓已寫好的測試自動執行。

這個專案也要放在你有權操作的 GitHub 儲存庫。流程檔 .github/workflows/e2e.yml 是從配套儲存庫根目錄算起,不是放在書稿目錄裡就會自動執行。

接著,確認 GitHub CLI 已安裝並登入。下面兩行在終端機執行;如果還沒登入,再用 gh auth login 完成登入:

bash
gh --version
gh auth status

最後是登入測試要用的帳密。tests/e2e/login.spec.ts 裡「用正確帳密登入」那支,會從 TOOLSHOP_TEST_USER 和 TOOLSHOP_TEST_PASS 這兩個環境變數讀帳號和密碼。本機可以在終端機設定,但雲端讀不到你電腦上的變數,所以要改放進儲存庫的 Settings → Secrets and variables → Actions。

值直接用練習站公開的範例顧客帳號,就是第 25 天用過的那組:TOOLSHOP_TEST_USER 填 customer@practicesoftwaretesting.com,TOOLSHOP_TEST_PASS 填 welcome01。如果登入失敗,先到練習站的登入頁確認範例帳號有沒有換過。習慣用終端機的話,也可以在配套儲存庫裡執行:

bash
gh secret set TOOLSHOP_TEST_USER --body "customer@practicesoftwaretesting.com"
gh secret set TOOLSHOP_TEST_PASS --body "welcome01"
gh secret list

gh secret list 列得出這兩個名稱就算設好,值不會顯示出來。沒設定的話,成功登入那支會跳過,其他三支照常執行,報告裡要照實寫出跳過了一支。

還有一件事先說清楚:後面會看當年的失敗紀錄,但配套程式現在已包含第 28 天的修復。你今天執行,乾淨版不一定會重現當時的錯,照實記下自己的結果就好,不必為了配合故事把它弄壞。

動手試試 ​

位置確認好後,我們回到助手對話,把這輪工作交給 ci-pipeline。你可以直接貼這段:

text
請用 ci-pipeline,把 tests/e2e 裡的四支測試接進 GitHub Actions。
沿用 tests/playwright.config.ts,先檢查 .github/workflows/e2e.yml。
分別跑 clean 與 with-bugs,不加入 broken 或 flaky 的示範。
一邊測試失敗,另一邊仍要繼續;各自保存報告、結構化結果與失敗證據。
檔名要能辨認環境與執行嘗試,並同步附件命名約定與下游讀取方式。
依現有授權處理修改與執行,交回執行連結、通過/失敗/跳過數和證據位置。

拿到助手的修改後,我們先看三件事:它有沒有只跑昨天那四支?兩個版本有沒有分開?測試失敗後,報告還會不會上傳?不需要一開始就把整份設定背起來。

下面提供一份完整的練習範例,方便核對。它比舊流程多上傳結構化結果,也把嘗試編號放進附件名稱,避免重跑後混淆。這是本次練習建議採用的設定,不是當年實驗的原始流程。

展開流程範例:.github/workflows/e2e.yml

這份範例沿用配套專案的報告路徑,保留七天。已有流程時,請助手比較差異後調整,並同步 references/artifact-contract.md 與下游讀取方式;不要把舊有的其他工作整份覆蓋掉。

yaml
name: e2e

on:
  push:
    branches: ['**']
    paths:
      - 'tests/**'
      - 'package.json'
      - 'package-lock.json'
      - '.github/workflows/e2e.yml'
  pull_request:
  workflow_dispatch:

jobs:
  e2e:
    name: e2e (${{ matrix.sut }})
    runs-on: ubuntu-latest
    timeout-minutes: 20
    strategy:
      fail-fast: false
      matrix:
        sut: [clean, with-bugs]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - name: 執行四支測試
        env:
          SUT: ${{ matrix.sut }}
          TOOLSHOP_TEST_USER: ${{ secrets.TOOLSHOP_TEST_USER }}
          TOOLSHOP_TEST_PASS: ${{ secrets.TOOLSHOP_TEST_PASS }}
        run: npx playwright test --config tests/playwright.config.ts tests/e2e
      - name: 保存閱讀用報告
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report-${{ matrix.sut }}-attempt-${{ github.run_attempt }}
          path: output/reports/playwright
          if-no-files-found: error
          retention-days: 7
      - name: 保存結構化結果
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results-json-${{ matrix.sut }}-attempt-${{ github.run_attempt }}
          path: output/runs/playwright-results.json
          if-no-files-found: error
          retention-days: 7
      - name: 保存失敗證據
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-artifacts-${{ matrix.sut }}-attempt-${{ github.run_attempt }}
          path: output/runs/playwright-artifacts
          if-no-files-found: warn
          retention-days: 7

前面的 matrix 會把同一套測試分成兩個工作,各跑一個版本。fail-fast: false 則避免其中一邊失敗,就取消另一邊。若你原有的流程另外設定「新執行取消舊執行」,還是可能中途被取消,要把取消狀態記下來。

檔名後面的 attempt 是這個執行編號第幾次嘗試。這份範例把它寫進名稱,稍後就能只下載對應那次的附件。舊版流程沒有這個後綴,不能直接套用同樣的篩選方式。

設定準備好了,但它還只在你的電腦上。GitHub 只會執行儲存庫裡的流程檔,所以跑第一輪之前,要先把這次的修改送上去。下面的指令都在配套專案根目錄執行;如果你授權助手提交和推送,也可以請它照這個順序做,再把每一步的輸出交回來給你核對。

1. 確認要推到哪個儲存庫。

bash
gh repo view --json nameWithOwner,defaultBranchRef,viewerPermission

nameWithOwner 應該是你自己的儲存庫,viewerPermission 要是 WRITE、MAINTAIN 或 ADMIN。記下 defaultBranchRef 的名稱,例如 main 或 master,後面會用到。如果顯示的是別人的儲存庫,或權限只有 READ,先 fork 一份或改用自己的儲存庫,否則之後會找不到執行按鈕。

2. 只提交這次相關的修改。

bash
git status
git switch -c ci/e2e-artifacts
git add .github/workflows/e2e.yml
git diff --cached --stat
git commit -m "ci: run e2e for clean and with-bugs, upload reports and results"

git add 這一行,請依助手交回的修改清單補上其他檔案,例如它同步改過的 references/artifact-contract.md。送出前看一眼 git diff --cached --stat:清單裡只該出現這次的設定。如果看到 output/、.env 或其他練習的檔案,先移出暫存區。

3. 推送分支。

bash
git push -u origin ci/e2e-artifacts

這份流程的 push 條件包含 .github/workflows/e2e.yml,所以推送後 GitHub 可能已經自動用這個分支跑了一輪。打開 Actions 看得到它的話,這一輪也能當作第一輪,記下執行編號就好。

4. 讓流程進入預設分支。

手動執行有兩個前提:預設分支上的流程檔要有 workflow_dispatch,而且你對儲存庫有寫入權限。如果你是第一次新增這個流程,就依儲存庫平常的做法,把它合併進預設分支,例如開合併請求:

bash
gh pr create --fill

合併要不要做、什麼時候做,由你決定。就算請助手代勞,也只讓它開合併請求,不要讓它自己合併。

如果預設分支上本來就有 e2e 流程,而且已經有 workflow_dispatch,就不用等合併。等一下啟動時選自己的分支,GitHub 就會用那個分支上的流程檔。

5. 確認 GitHub 上是你要跑的版本。

bash
gh workflow view e2e.yml --ref ci/e2e-artifacts --yaml | grep -n -E "workflow_dispatch|run_attempt"

--ref 換成你要執行的分支。兩個關鍵字都有出現,代表 GitHub 上那個分支已經有新設定:可以手動啟動,附件名稱也會帶嘗試編號。只看到 workflow_dispatch,表示那個分支上還是舊版流程。

6. 手動啟動第一輪。

打開 Actions,在左側點 e2e。停在「All workflows」時不會出現按鈕。接著按右上方的 Run workflow,在分支選單選好要測的分支後啟動。習慣用終端機的話,也可以執行:

bash
gh workflow run e2e.yml --ref ci/e2e-artifacts
gh run list --workflow e2e.yml --event workflow_dispatch --limit 1

推送和合併也會觸發執行,所以列表裡不一定只有你手動按的那一輪。這裡加上 --event workflow_dispatch,只列出手動啟動的執行。記下它的執行編號,再對一下分支和時間是不是這一輪。

找不到 Run workflow 時,依序檢查這幾件事:左側有沒有選 e2e?預設分支上的流程有沒有 workflow_dispatch?你的權限是不是 READ?如果是 fork 來的儲存庫,有沒有先到 Actions 分頁啟用流程?細節可以看 GitHub 手動執行說明。

進入這次執行頁面,你應該會看到 e2e (clean) 和 e2e (with-bugs)。等兩邊都結束,再往下看結果。之後符合設定的推送或合併請求也會自動觸發,不必每次手動按。

跑完之後 ​

先別只看最上面的紅綠燈。這套教材刻意測含缺陷版,它若抓到原本的金額錯誤,整輪仍可能顯示失敗。要分別看兩個工作,確認各自在哪裡失敗,不能把「整輪變綠」當成今天唯一的完成條件。

我們先用當年的第一輪做對照。執行編號是 30710272941,後來重跑失敗項目,編號不變,但嘗試編號從 1 變成 2:

執行嘗試乾淨版 clean含缺陷版 with-bugs
第 1 次1 通過、2 失敗、1 跳過1 通過、2 失敗、1 跳過
第 2 次1 通過、2 失敗、1 跳過1 通過、2 失敗、1 跳過

兩邊都是四支測試,但失敗訊息不同:乾淨版等不到購物車元素,含缺陷版則是金額應為 14.15、實際卻是 0。原因明天再查,現在先把這個差異記下來。

至於跳過的那一支,是當時雲端沒設定登入帳密。昨天本機有跑,不代表搬到雲端也一定有跑,這也是我們要把總數一起列出的原因。

知道要看哪些結果後,接著把你這一輪的資料拿回來。在 sdet-skills/ 的終端機列出近期執行:

bash
gh run list --workflow e2e.yml --limit 5

找到剛才那一輪的編號。下面這段執行後會先等你輸入,把編號貼上,再按 Enter:

bash
read -r run_id
gh run view "$run_id" --json databaseId,attempt,url,headBranch,headSha,event,status,conclusion,jobs

先確認連結和版本是你要的,狀態也已經結束。attempt 會告訴你目前是第幾次嘗試。接著保存這次的摘要、失敗紀錄與附件:

bash
run_attempt=$(gh run view "$run_id" --json attempt --jq .attempt)
evidence_dir="output/sessions/20260925_ci-e2e/run-${run_id}-attempt-${run_attempt}"
mkdir -p "$evidence_dir"
gh run view "$run_id" --attempt "$run_attempt" \
  --json databaseId,attempt,url,headBranch,headSha,event,status,conclusion,jobs > "$evidence_dir/run.json"
gh run view "$run_id" --attempt "$run_attempt" --log-failed > "$evidence_dir/failed.log"
gh run download "$run_id" --pattern "*-attempt-${run_attempt}" --dir "$evidence_dir/artifacts"
printf '%s\n' "$evidence_dir"

這裡用 20260925_ci-e2e 當練習目錄,你可以換成自己的日期與名稱。指令預設查目前配套儲存庫;若 CLI 指向的不是你剛才操作的儲存庫,就在各個 gh run 指令補上 --repo 擁有者/儲存庫。查看紀錄、下載附件

GitHub 把這些附件叫做 artifact。依上面的流程範例,第一次嘗試下載成功後,目錄會像下面這樣。這是預期結構示例,不是當年的下載結果:

text
artifacts/
├── playwright-report-clean-attempt-1/
│   └── index.html
├── playwright-report-with-bugs-attempt-1/
│   └── index.html
├── test-results-json-clean-attempt-1/
│   └── playwright-results.json
├── test-results-json-with-bugs-attempt-1/
│   └── playwright-results.json
└── playwright-artifacts-with-bugs-attempt-1/
    └── …截圖、操作紀錄等失敗證據

失敗證據只在有產生檔案時才會有。乾淨版如果也失敗,就另外檢查它的證據目錄;不要因為示例沒畫出來,就把它漏掉。

報告可以請 Playwright 開啟。這行沿用剛才的目錄與嘗試編號,先看乾淨版:

bash
npx playwright show-report "$evidence_dir/artifacts/playwright-report-clean-attempt-${run_attempt}"

打開後,核對通過、失敗、跳過的數量,並確認失敗項目的證據連結能開。failed.log 若是空的,也要回頭看執行狀態,不能只憑空檔就認定全部通過。

資料都在手上了,最後把這一輪整理成一份交接摘要。明天可能換一段助手對話,它不會記得今天看過什麼;只要把這份摘要的路徑交給它,就能找回同一次執行、同一次嘗試和同一批附件。

摘要放在剛才的證據目錄裡,檔名叫 handoff.md。下面是空白模板,每一格都要填你這一輪的實際值:

展開交接摘要模板:handoff.md
markdown
# 第 27 天交接摘要

## 這一輪是哪一次

- 儲存庫:
- 分支:
- commit:
- 執行連結:
- 執行編號:
- 嘗試編號:
- 觸發方式:
- 整輪結論:
- 證據目錄:
- 失敗紀錄:

## 兩邊的測試計數

| 工作 | 工作結論 | 通過 | 失敗 | 跳過 | 總數 |
| --- | --- | --- | --- | --- | --- |
| e2e (clean) |  |  |  |  |  |
| e2e (with-bugs) |  |  |  |  |  |

## 失敗項目

| 工作 | 測試 | 失敗訊息第一行 | 證據路徑 |
| --- | --- | --- | --- |
|  |  |  |  |

## 產物位置

| 類別 | clean | with-bugs |
| --- | --- | --- |
| 閱讀用報告 |  |  |
| 結構化結果 |  |  |
| 失敗證據 |  |  |

## 缺少的資料

- 

## 計數來源與備註

-

欄位不用自己一格格抄,交給助手讀檔填寫就好。回到助手對話,貼上這段,並把 證據目錄 換成剛才終端機印出的路徑:

text
請讀取證據目錄裡的 run.json、failed.log 和 artifacts/,依第 27 天的模板建立 handoff.md。
證據目錄:(貼上 evidence_dir)
儲存庫、commit、執行編號與嘗試編號以 run.json 為準;計數以兩邊的 playwright-results.json 為準。
路徑都寫從配套專案根目錄算起的完整相對路徑,並確認檔案真的存在。
找不到的附件或讀不到的欄位,寫進「缺少的資料」,不要留白,也不要拿別次執行補上。
這一輪只整理,不分析原因、不修測試。

交回後,自己抽查兩件事。第一,計數要對得上報告。結構化結果裡的 stats 就是計數來源:

bash
jq '.stats' "$evidence_dir/artifacts/test-results-json-clean-attempt-${run_attempt}/playwright-results.json"

expected 是通過,unexpected 是失敗,skipped 是跳過;總數是這幾項加上 flaky 的合計。這個專案關閉了重試,flaky 應該是 0。with-bugs 那份把路徑裡的 clean 換掉再看一次。

第二,摘要裡的路徑要打得開。每一條都用 ls 試一次;打不開的,就移到「缺少的資料」。登入測試若被跳過,也要寫在備註,說明是雲端沒有帳密,還是其他原因。

做到這裡,今天的交付就完整了:一份 handoff.md,加上它指到的報告、結構化結果和失敗證據。

如果想查看本篇的歷史紀錄

下面這行指定當年的第一次嘗試,不會混到後來的重跑:

bash
gh run view 30710272941 --repo vansleee/sdet-skills --attempt 1 --log-failed

2026 年 9 月 25 日核對時,文字紀錄仍讀得到,但當年的附件已過期,下載回傳 no valid artifacts found to download。附件原本保留七天,所以今天跟做時,要用自己的近期執行。

如果要復盤當年的故事,可以對照配套專案 output/sessions/2026-08-02_ci-e2e-first-run/failure-analysis.yaml 的既有分析;它不等於已下載到原始截圖。下一篇會沿著當時留下的紀錄講解,你自己這輪則依實際拿到的證據判斷。

若下載沒有成功,先保留 run.json 和 failed.log,記下錯誤原因。不要把空目錄交出去,卻說證據都齊了。

背後怎麼做 ​

走完一輪,再回頭看設定,就比較容易理解為什麼要存三種資料:報告方便人閱讀,結構化結果方便助手整理每支測試,截圖和操作紀錄則用來核對當時發生什麼。下一篇查原因時,會各自用到。

報告與結構化結果使用 if: always(),讓前面的測試失敗後仍會嘗試上傳;這不保證檔案一定存在。所以範例再加上 if-no-files-found: error,少了應交的報告就明確報錯。失敗證據則用 if: failure();如果連安裝都失敗,可能還沒產生截圖,缺少的部分要記錄,不能憑空補出來。上傳工具說明

名稱也要一起對好。舊流程只上傳報告和失敗證據,沒有上傳那份結構化結果;下游約定也還有未分環境的舊名稱。因此今天請助手補設定時,要把附件名稱、來源路徑與下游讀取方式一併核對。只改上傳端,下一位仍可能找不到檔案。

現在,收尾前請助手交一份簡短摘要:

  • 這次的儲存庫、版本、執行連結與嘗試編號。
  • 兩個環境各自通過、失敗、跳過及總數。
  • 報告、結構化結果與失敗證據的位置;缺什麼也一起列出。

拿這份摘要和實際檔案對一次,才算完成今天的練習。夜間排程、介面測試分工,以及第 29 天的重複量測,都可以等這條流程接穩後再加。

今天學到什麼 ​

從第 25 天到今天,我們先確認測試的答案,再把要檢查的行為寫成測試,現在也接上了自動執行。不過,跑得自動不代表判得正確;每輪還是要留下版本、環境與證據,才能知道哪裡失敗、實際跑了多少,以及接下來能查什麼。

資料準備好後,明天就帶著這份 handoff.md,把它交給負責分析的同事。我們會回到乾淨版那次失敗,看看為什麼我猜錯兩次,又是哪些證據讓判斷轉了方向。


作者備忘錄 — 正式出版前應移除

章節分工

整體審查範圍為第 24~30 天。第 24 天檢查候選,放行後仍需經分派建立問題單;第 25 天用本機權限修復案例,建立「先核對判準,再用測試驗證修復」的觀念;第 26 天換購物車案例,交回測試與本機結果;本篇接上自動執行並完成取證;第 28 天查因、分派與回顧修復;第 29 天再換搜尋測試練習量測不穩定;第 30 天用新一輪值班收尾,區分已串接的缺陷流程與另行啟動的測試維護。原標題保留故事懸念,前言先交代本篇範圍。

第 25 天的補跑日期是 8 月 10 日,第 27、28 天的歷史案例則是 8 月 2 日。章節依教學順序排列,不是同一次修復的逐日紀錄;本篇也不以購物車結果宣稱權限問題已修復。七篇操作與交接分析見書稿專案 reviews/chapter27/context-24-30.md。

案例與範例的差別

  • 正文的新流程是供讀者練習的建議設定,增加結構化結果、嘗試編號後綴與缺檔處理,並將 package-lock.json 納入推送條件。配套專案的舊流程、命名約定與讀取技能並未因本篇改稿而更新,不可宣稱已完成整條雲端驗證。
  • 自己的操作結果與歷史案例分開。最新版共用操作已有修復,不要求讀者重現舊導頁失敗。
  • 原圖最後兩列的「未跑」有誤,本篇移除該圖。2026-09-25 查證,30710750654、30710799963 都有執行含缺陷版,且因金額斷言失敗。
  • 後續三次驗收編號為 30710677728、30710750654、30710799963。三次都是乾淨版工作成功,含缺陷版工作失敗;完整修復與驗收留在第 28 天說明。
  • 歷史實跑、附件到期與命名落差的審查證據保存在書稿專案 reviews/chapter27/。新範例的檢查結果需與這批歷史證據分開保存。

參考資料 ​

  1. GitHub:手動執行流程 — 啟動條件與操作入口。
  2. GitHub:多組設定的工作 — 環境矩陣與失敗時的取消行為。
  3. GitHub CLI:查看執行紀錄、下載附件 — 執行編號、嘗試編號與附件篩選。
  4. 上傳附件工具第 4 版 — 上傳內容、保留天數與缺檔處理。
  5. 配套專案 .github/workflows/e2e.yml、tests/playwright.config.ts、references/artifact-contract.md、skills/infra/pipeline-read/SKILL.md — 本篇設定與交接位置的核對來源。
  6. 配套專案 output/sessions/2026-08-02_ci-e2e-first-run/ — 原始分析與修復驗收紀錄。