DE Day 44 部署與展示
執行需求:CPU 可跑。今天是專案篇的最後一天實作。我們要把 Day 42 的 GitHub Actions 排程推上雲端、把 Day 43 的指標接到 Streamlit 儀表板、並把這套管線「開放」給所有人瀏覽。具體要做四件事:第一,把 DuckDB 檔案與指標快照變成可下載的 artifact;第二,用 Streamlit 1.41 寫一支儀表板應用(讀 project_config.INDICATORS 與 mart_daily_summary);第三,把儀表板部署到 Streamlit Community Cloud 或自架容器;第四,把 GitHub Actions 的 workflow 完整接上 daily_run、回歸測試、文件檢查。整個部署流程沿用 Day 41-43 的共用設定,Day 45 會用今天的成果做系列總結。
引言
資料工程的「最後一哩」是讓資料被看到、被使用。寫得再好的管線,如果沒有人查詢儀表板、沒有報告引用指標,就只是一堆跑得很快的程式碼。今天我們把專案從「能跑」推進到「能展示」,這個轉變比想像中大:它牽涉到部署、權限、檔案大小、讀延遲、UI 設計、可存取性。我們一步一步把這些都處理好。
「展示」分成兩個層次。第一層是「資料分析師介面」,給 SQL 熟練的人用,他們需要直接 query DuckDB 拿最新資料。第二層是「一般使用者介面」,給完全沒碰過 SQL 的同事或對外民眾用,他們透過 Streamlit 的儀表板看圖表、看指標。本專案的目標使用者包含兩種,所以兩種介面都要做:dbt 的 docs serve 提供第一層、Streamlit 提供第二層。
今天的設計原則是「可重現、可存取、可驗證」。可重現指任何同事都能從空白機器跑 bash scripts/bootstrap.sh 把整個環境建起來;可存取指部署後的 URL 任何人能開、不需要登入;可驗證指部署後的應用要能被自動測試確認「資料有更新、指標有計算、儀表板有渲染」。我們把這三個特性都做到。
第一段部署:把 DuckDB 變成 artifact
GitHub Actions 跑完 daily_run 後,warehouse/de-journey.duckdb 與 logs/indicators/*.json 都應該被儲存起來。預設 GitHub Actions 工作目錄是 ephemeral(每次重來都乾淨),所以我們要把產出物上傳成 artifact。我們在 Day 42 的 workflow 加上上傳步驟:
# de-journey/.github/workflows/daily_aqi.yml(補充)
- name: 上傳 warehouse 與指標快照
if: always()
uses: actions/upload-artifact@v4
with:
name: aqi-artifacts-${{ github.run_id }}
path: |
warehouse/de-journey.duckdb
logs/indicators/
retention-days: 30
這段把 DuckDB 檔案與指標快照上傳成 artifact。retention-days: 30 讓 artifact 在 GitHub 保留 30 天(之後會自動刪除,避免無限增長)。if: always() 表示即使前面步驟失敗也上傳,這樣事後除錯時可以拿到失敗當下的資料夾狀態。
另一個做法是用 Pages 把 artifact 直接公開:
- name: 發布到 gh-pages(讓任何人下讀)
if: success()
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
destination_dir: data/${{ github.run_id }}
peaceiris/actions-gh-pages@v4 是社群最常用的 Pages 部署 action。destination_dir: data/${{ github.run_id }} 把每次跑的 DuckDB 與快照放到獨立目錄,保留歷史;可下載的 URL 是 https://<org>.github.io/<repo>/data/<run_id>/de-journey.duckdb。這個 URL 對外公開,沒有存取限制。
第二段部署:Streamlit 儀表板
Streamlit 1.41 是 2025 年最受歡迎的 Python 儀表板框架。把指標與事實表視覺化成儀表板只要幾十行程式碼,而且不用寫 HTML/CSS。我們寫一支讀 DuckDB 與指標 JSON 的應用:
"""de-journey/dashboards/aqi_app.py:Streamlit AQI 儀表板。"""
from __future__ import annotations
import sys
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
import duckdb
import pandas as pd
import streamlit as st
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "pipelines"))
from project_config import INDICATORS, MODELS, PROJECT # noqa: E402
st.set_page_config(page_title="AQI 儀表板", layout="wide")
st.title("全國空氣品質儀表板")
st.caption(f"資料來源:{PROJECT['source']['name']}({PROJECT['license']})")
WAREHOUSE = Path(__file__).resolve().parents[1] / "warehouse" / "de-journey.duckdb"
@st.cache_data(ttl=3600)
def load_fact() -> pd.DataFrame:
con = duckdb.connect(str(WAREHOUSE), read_only=True)
df = con.execute(
"SELECT observed_at, station_id, county, aqi, pm25 "
"FROM aqi.fct_aqi_hourly "
"WHERE observed_at >= current_timestamp - INTERVAL '30 days'"
).df()
con.close()
return df
@st.cache_data(ttl=3600)
def load_indicators(target: str) -> dict:
p = Path(__file__).resolve().parents[1] / "logs" / "indicators" / f"{target}.json"
if not p.exists():
return {}
import json
return json.loads(p.read_text(encoding="utf-8"))
df = load_fact()
col1, col2, col3, col4 = st.columns(4)
col1.metric("近 30 天資料筆數", f"{len(df):,}")
col2.metric("測站數", f"{df['station_id'].nunique()}")
col3.metric("平均 AQI", f"{df['aqi'].mean():.1f}")
col4.metric("PM2.5 平均", f"{df['pm25'].mean():.1f}")
st.subheader("全國 AQI 趨勢")
ts = df.set_index("observed_at").resample("D")["aqi"].mean()
st.line_chart(ts)
st.subheader("各測站平均 AQI 排名")
ranking = df.groupby("station_id")["aqi"].mean().sort_values(ascending=False).head(10)
st.bar_chart(ranking)
這段把 Streamlit 儀表板寫完。@st.cache_data(ttl=3600) 是關鍵設計:Streamlit 會把函式結果快取 1 小時,避免每次互動都重新 query DuckDB。st.metric 把指標做成大數字卡片,st.line_chart、st.bar_chart 把趨勢與排名做成內建圖表(不用寫 matplotlib)。整支儀表板不到 50 行程式碼,視覺效果卻相當完整。
執行:
cd de-journey
uv run streamlit run dashboards/aqi_app.py --server.port 8501
# 打開 http://127.0.0.1:8501 就能看到儀表板
本機執行後用瀏覽器開 http://127.0.0.1:8501,會看到四個指標卡片、一張線图(30 天 AQI 趨勢)、一張柱狀圖(測站排名)。整個 UI 響應式設計,手機、平板、桌機都能正常瀏覽。
第三段部署:Streamlit Community Cloud
Streamlit Community Cloud 是免費的 Streamlit 部署服務,只要把 repo 推上 GitHub、連結 Streamlit 帳號就能部署。我們把儀表板部署上去:
# de-journey/.streamlit/config.toml
[server]
headless = true
port = 8501
[theme]
base = "light"
primaryColor = "#1f77b4"
[browser]
gatherUsageStats = false
這個設定檔放在 de-journey/.streamlit/config.toml。headless = true 表示不要開瀏覽器自動跳轉,適合部署環境。primaryColor 是 Streamlit 主色,用藍色(#1f77b4)配合 AQI 儀表板的清爽風格。gatherUsageStats = false 關閉遙測。
部署步驟(在 Streamlit Community Cloud UI 操作):
- 登入 share.streamlit.io(用 GitHub 帳號)。
- 點「New app」,選
your-org/de-journey、branchmain、main filedashboards/aqi_app.py。 - 點「Deploy」,等 1–2 分鐘就會拿到一個
https://your-app.streamlit.app的網址。
部署後的 URL 任何人能開,不需要登入。這是本專案的「公開展示」入口。Streamlit Community Cloud 在 2025 年仍是免費的(公開 repo),對於 side project 與學習型專案是最方便的部署選項。
Streamlit Community Cloud 在部署時會自動讀 requirements.txt(或 pyproject.toml)安裝套件。我們用一份精簡的 requirements.txt 把部署用的套件列出來,避免部署階段安裝 dev 套件導致時間過長:
# de-journey/dashboards/requirements.txt
duckstreamlitplotpandas
這份 requirements.txt 只列部署用的套件,比 pyproject.toml 的 dependencies 精簡很多(沒有 dbt-core、dbt-duckdb、APScheduler 等只在 pipeline 用的套件)。uv pip compile 可以從 pyproject.toml 自動產生這份檔,但我們手動維護更靈活。實務上你也可以讓 Streamlit Community Cloud 直接讀 uv.lock(透過 runtime.txt 指定 Python 3.13 + uv pip install),但需要較多的 CI 設定。
部署後的 Streamlit 應用可以加上一個「地圖檢視」分頁,把測站位置與最近 24 小時 AQI 用 st.map 顯示:
"""de-journey/dashboards/aqi_map.py:地圖檢視分頁。"""
from __future__ import annotations
from pathlib import Path
import duckdb
import streamlit as st
WAREHOUSE = Path(__file__).resolve().parents[1] / "warehouse" / "de-journey.duckdb"
@st.cache_data(ttl=3600)
def load_station_aqi() -> object:
con = duckdb.connect(str(WAREHOUSE), read_only=True)
df = con.execute("""
SELECT s.station_id, s.station_name, s.county,
s.latitude, s.longitude,
AVG(f.aqi) AS avg_aqi
FROM aqi.dim_stations s
JOIN aqi.fct_aqi_hourly f ON s.station_id = f.station_id
WHERE f.observed_at >= current_timestamp - INTERVAL '24 hours'
AND s.latitude IS NOT NULL
GROUP BY s.station_id, s.station_name, s.county,
s.latitude, s.longitude
""").df()
con.close()
return df
st.subheader("近 24 小時各測站 AQI(地圖)")
df = load_station_aqi()
st.map(df, latitude="latitude", longitude="longitude", size="avg_aqi",
color="avg_aqi")
st.map 是 Streamlit 內建的地圖元件,傳入 latitude、longitude 就會自動渲染地圖。size="avg_aqi" 把圓點大小對應 AQI 平均值,color="avg_aqi" 把顏色深淺也對應 AQI(越紅越糟)。這是「對外展示」最直覺的視覺化,民眾打開儀表板就能看到「我家附近的空氣現在怎麼樣」。
第四段部署:GitHub Pages 部署文件站
Day 40 的文件站也要部署。沿用 mkdocs gh-deploy:
# de-journey/.github/workflows/docs.yml
name: Build and Deploy Docs
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-deploy:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
with:
python-version: '3.13'
- run: uv sync
- run: uv run mkdocs build --strict
- uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
這個 workflow 在每次 main 分支有 push 時自動建文件站、推到 gh-pages 分支。mkdocs build --strict 是重點:--strict 把任何 warning 視為錯誤,避免「broken link 沒被發現」這種問題。部署後的文件站 URL 是 https://your-org.github.io/de-journey/。
第五段部署:完整 CI 流程
把 daily_run、回歸測試、文件檢查、儀表板測試四個 CI 工作串起來:
# de-journey/.github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
with:
python-version: '3.13'
- run: uv sync
- name: 套件鎖檢查
run: uv lock --check
- name: 環境驗證
run: uv run python scripts/verify_env.py
- name: 模型清單檢查
run: uv run python scripts/check_models.py
- name: 套用合約檢查
run: uv run python scripts/check_contract.py
- name: dbt run(CI 子集)
working-directory: models/aqi_project
run: uv run dbt run --select staging intermediate
- name: 文件連結檢查
run: uv run python scripts/check_links.py
- name: 文件指令檢查
run: uv run python scripts/doc_doctor.py
這是 PR 與 push 都會觸發的 CI 工作。uv lock --check 確保 PR 沒有忘記 lock;verify_env.py 確保環境一致;check_models.py 確保模型清單與檔案一致;check_contract.py 跑合約檢查;dbt run --select staging intermediate 只跑 staging 與 intermediate(marts 需要真實資料,CI 跑會比較慢);check_links.py 與 doc_doctor.py 是 Day 40 寫的文件檢查。任何一步失敗 PR 就不能合併。
部署後的完整工作流
把所有段拼起來,整個專案的部署與運行長這樣:
- 本地開發:
uv run python pipelines/daily_run.py --date 2025-12-09,或uv run streamlit run dashboards/aqi_app.py。 - 本機排程:
uv run python pipelines/scheduler.py(APScheduler 3.11,每天 03:00)。 - 雲端排程:GitHub Actions
daily_aqi.yml(每天定時跑,產生 artifact)。 - 公開展示:Streamlit Community Cloud(一般使用者看的儀表板)。
- 公開文件:GitHub Pages(Day 40 的 MkDocs 站)。
- 品質閘門:GitHub Actions
ci.yml(PR 檢查)。
這六個入口都讀同一份 project_config.py、同一個 DuckDB 檔案、同一份合約。任何入口的改動都會反映到其他入口,這就是 Day 41 強調的「共用設定的單一來源」。
驗證部署是否成功
部署完成後要驗證。我們寫一支「部署冒煙測試」腳本,模擬「使用者打開儀表板」的行為:
"""de-journey/tests/e2e/test_dashboard.py:驗證 Streamlit 儀表板跑得起來。"""
from __future__ import annotations
import subprocess
import time
from pathlib import Path
import requests
ROOT = Path(__file__).resolve().parents[2]
def test_dashboard_starts():
proc = subprocess.Popen(
["uv", "run", "streamlit", "run", "dashboards/aqi_app.py",
"--server.port", "8502", "--server.headless", "true"],
cwd=str(ROOT), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
time.sleep(8) # 給 Streamlit 8 秒啟動
try:
resp = requests.get("http://127.0.0.1:8502/_stcore/health", timeout=5)
assert resp.status_code == 200, f"儀表板沒回應:{resp.status_code}"
finally:
proc.terminate()
proc.wait(timeout=10)
print("儀表板冒煙測試通過")
這支 E2E 測試啟動 Streamlit、檢查 health endpoint、結束 Streamlit。time.sleep(8) 給 Streamlit 啟動時間;/_stcore/health 是 Streamlit 內建 health endpoint,回 200 就代表啟動成功。這個測試可以接進 CI,每天驗證一次「部署後的版本仍能跑」。
除了儀表板冒煙測試,我們再加一支「artifact 下載驗證」腳本:CI 工作跑完後,從 GitHub API 下載最新一份 artifact、確認 DuckDB 與指標快照都能解開。這是「部署成功的端到端驗證」:
"""de-journey/scripts/verify_artifact.py:驗證最新一份 artifact 可下載且可解開。"""
from __future__ import annotations
import json
import os
import zipfile
from pathlib import Path
from urllib.request import urlopen
REPO = os.environ["GITHUB_REPOSITORY"]
TOKEN = os.environ["GITHUB_TOKEN"]
api = f"https://api.github.com/repos/{REPO}/actions/artifacts?per_page=1"
req = urlopen(urlopen(api).headers.get("Link", "").split(";")[0]
if "Link" in urlopen(api).headers else api)
arts = json.loads(urlopen(api).read().decode("utf-8"))
if not arts["artifacts"]:
raise SystemExit("找不到任何 artifact,請確認 daily_aqi workflow 跑過")
latest = arts["artifacts"][0]
print(f"最新 artifact:{latest['name']}({latest['size_in_bytes']} bytes)")
zip_path = Path("artifact.zip")
zip_path.write_bytes(urlopen(latest["archive_download_url"]).read())
with zipfile.ZipFile(zip_path) as zf:
members = zf.namelist()
has_duckdb = any(m.endswith("de-journey.duckdb") for m in members)
has_indicators = any("indicators/" in m for m in members)
print(f"解壓後 {len(members)} 個檔案:DuckDB={has_duckdb}, "
f"indicators={has_indicators}")
if not (has_duckdb and has_indicators):
raise SystemExit("artifact 缺少必要檔案")
這支腳本透過 GitHub API 拿最新 artifact 的下載 URL,再用 urlopen 抓回本地解壓。沒有用 requests 或 PyYAML 額外套件,全靠標準函式庫。當 artifact 缺 DuckDB 或缺指標快照時,腳本會以非零 exit code 退出,讓 CI 工作失敗。
接著我們寫一支「部署通知」腳本,把今天部署的關鍵指標推到 Slack 或 Email。這是 Day 38 告警流的延伸,從「契約違破」推進到「部署結果通知」:
"""de-journey/scripts/notify_deploy.py:把部署結果寫進 Slack 或 log。"""
from __future__ import annotations
import json
import os
import urllib.request
from datetime import date
from pathlib import Path
REPORT_DIR = Path(__file__).resolve().parents[1] / "logs" / "indicators"
SLACK_URL = os.environ.get("SLACK_WEBHOOK_URL", "") # 沒設定就只寫 log
def send_slack(text: str) -> bool:
if not SLACK_URL:
return False
payload = json.dumps({"text": text}).encode("utf-8")
req = urllib.request.Request(
SLACK_URL, data=payload,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=10) as resp:
return resp.status == 200
if __name__ == "__main__":
today = date.today().isoformat()
p = REPORT_DIR / f"{today}.json"
if not p.exists():
print(f"{today} 無指標快照,跳過通知")
else:
m = json.loads(p.read_text(encoding="utf-8"))
text = (f"[OK] AQI 部署完成 ({today}): "
f"AQI={m.get('national_daily_aqi_avg')}, "
f"PM2.5={m.get('monthly_pm25_avg')}")
if send_slack(text):
print("已推 Slack")
else:
print(f"未設 SLACK_WEBHOOK_URL,僅顯示:{text}")
SLACK_WEBHOOK_URL 用 os.environ 讀取,不寫死任何金鑰。如果環境變數沒設定,就只把訊息印到終端機、不推 Slack。這是 Day 39 教過的「金鑰管理」紀律的延伸:所有 webhook URL、API token 都不能出現在程式碼。
常見錯誤與踩雷
第一個雷:把 DuckDB 直接 commit 到 repo。DuckDB 是二進位檔案,每次跑 daily_run 都會改變,commit 會讓 git 歷史爆炸。請把 warehouse/ 與 logs/ 加入 .gitignore,改用 artifact 上傳。需要本地開發時,從 artifact 下載即可。
第二個雷:Streamlit Community Cloud 連不到內網資料。Streamlit Community Cloud 跑在他們的雲端,沒辦法存取你的內網服務。如果你的 ingest 需要 VPN 或內網 API,請先把資料同步到公開的雲端(GitHub artifact 或 S3),再讓 Streamlit 讀公開來源。
第三個雷:把 API 金鑰寫進 Streamlit 程式碼。當部署到公開的雲端時,任何人都能 fork 你的 repo。把金鑰放在 st.secrets(Streamlit Community Cloud 的機密管理)或 GitHub Actions 的 secrets,程式碼裡只讀 os.environ["KEY"]。
第四個雷:忘記設定 Streamlit 的 Python 版本。Streamlit Community Cloud 預設用 Python 3.12,但我們的 pyproject.toml 要求 3.13。請在 .streamlit/config.toml 或 Streamlit Cloud UI 明確指定 Python 版本。
第五個雷:MkDocs build 沒用 --strict。--strict 把 warning 升級成 error,能在 PR 階段就抓出 dead link、broken anchor 等問題。Day 40 文件站的範例已經有 --strict,這裡再強調一次。
為了讓部署的入口一目了然,我們再寫一支「部署入口腳本」:把所有部署相關的指令(bootstrap、daily_run、scheduler、streamlit、mkdocs)整理成一個 CLI,方便在文件站的首頁引用:
"""de-journey/scripts/deploy.py:把部署相關指令集中成一個 CLI。"""
from __future__ import annotations
import argparse
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def run(cmd: list[str]) -> None:
print("$", " ".join(cmd))
subprocess.run(cmd, cwd=str(ROOT), check=True)
def cmd_bootstrap(_args) -> None:
run(["bash", "scripts/bootstrap.sh"])
def cmd_daily(args) -> None:
run(["uv", "run", "python", "pipelines/daily_run.py", "--date", args.date])
def cmd_scheduler(_args) -> None:
run(["uv", "run", "python", "pipelines/scheduler.py"])
def cmd_streamlit(_args) -> None:
run(["uv", "run", "streamlit", "run", "dashboards/aqi_app.py"])
def cmd_docs(_args) -> None:
run(["uv", "run", "mkdocs", "build", "--strict"])
COMMANDS = {
"bootstrap": cmd_bootstrap,
"daily": cmd_daily,
"scheduler": cmd_scheduler,
"streamlit": cmd_streamlit,
"docs": cmd_docs,
}
if __name__ == "__main__":
p = argparse.ArgumentParser()
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("bootstrap")
p_daily = sub.add_parser("daily")
p_daily.add_argument("--date")
sub.add_parser("scheduler")
sub.add_parser("streamlit")
sub.add_parser("docs")
args = p.parse_args()
COMMANDS[args.cmd](args)
這支腳本把 Day 40-44 所有部署相關的指令集中到一個 CLI。python scripts/deploy.py bootstrap 等同 bash scripts/bootstrap.sh,python scripts/deploy.py daily --date 2025-12-09 跑 daily_run。文件站 README 只要寫「跑 python scripts/deploy.py bootstrap 就能把環境建好」這一句話,讀者不用記所有細節。deploy.py 的好處是「指令可以從 PR review 開始就保持一致」:當有人加了新的部署步驟,只要在這個檔案加一行 sub-parser,文件站的指示也自動同步。
效能與實務提醒
Streamlit Community Cloud 的免費方案在 2025 年仍可用,限制是「單一 app、有限記憶體、public repo only」。對於學習型專案與 side project 綽綽有餘;如果需要更多資源(GPU、長時間背景任務、private repo),可以升級付費或自架容器。
實務上有三個取捨值得記得。第一,部署選項:Streamlit Cloud vs Hugging Face Spaces vs 自架 Docker——前三個都是免費公開,自架需要 DevOps 知識。對學習型專案,Streamlit Cloud 是最低摩擦的選項。第二,儀表板快取:用 @st.cache_data 把 DuckDB query 快取起來,避免每次互動都重算。對長一點的查詢(最近 30 天),TTL 設 1 小時就夠。第三,CI 速度:CI 工作不要跑太多步驟,否則工程師會放棄 PR。建議只在 main push 跑完整測試,PR 只跑必要的(lint、合約檢查、staging 跑通)。
另一個工程上的提醒:部署後的 URL 是 https://your-app.streamlit.app,沒有自訂網域。對 side project 沒問題;如果要正式上線,可以花錢買自訂網域並用 Cloudflare 設定轉址。整個部署階段的時間估算:第一次部署(從 git push 到看到 Streamlit 畫面)約 5 分鐘;之後每次部署約 2–3 分鐘;GitHub Pages 文件站部署約 1 分鐘。
部署之後的監控與維運也要有心理準備。Streamlit Community Cloud 偶爾會重啟應用(特別是底層升級時),重啟後 @st.cache_data 會失效,需要重新 query DuckDB。建議在儀表板加一個「最後更新時間」顯示,讓使用者知道資料是幾點的快照。如果發現使用者常需要「強制重新整理」,可以把快取 TTL 從 3600 秒調到 600 秒(10 分鐘),這樣重啟後最長 10 分鐘就會有新資料。
小結
今天把專案篇的「部署與展示」蓋起來。我們把 DuckDB 與指標快照上傳成 GitHub artifact,用 Streamlit 寫了儀表板、部署到 Streamlit Community Cloud,把 MkDocs 文件站部署到 GitHub Pages,並把 CI 流程(PR 檢查、文件檢查、模型清單檢查、合約檢查)全部串起來。重點觀念有三個:第一,展示分兩層(SQL 介面給分析師、Streamlit 給一般使用者),兩種介面共用同一個 DuckDB;第二,部署選項要看場景,Streamlit Cloud 是學習型專案最低摩擦的選擇;第三,CI 把「每次 PR 都跑合約與文件檢查」這件事自動化,避免過期。
明天(Day 45)我們會做系列總結:回顧這 45 天的學習路徑、貫穿四天專案篇的所有設計、列出延伸學習方向,並對整個系列的工具鏈做最後一次盤點。Day 41-44 的共用設定(project_config.py、aqi_hourly.yml、七個 dbt 模型、四個層次的評估)在明天會再次出現,作為「這四天我們做到了什麼」的具體證明。今天的部署成果可以整理成一張「部署入口一覽表」放在文件站首頁:
| 入口 | URL/指令 | 更新頻率 | 適用情境 |
|---|---|---|---|
| Streamlit 儀表板 | https://your-app.streamlit.app |
1 小時快取 | 一般使用者 |
| GitHub Pages 文件站 | https://your-org.github.io/de-journey/ |
每次 push | 開發者、文件讀者 |
| dbt docs | uv run dbt docs serve |
dbt run 後 | 資料分析師 |
| DuckDB 檔案 artifact | GitHub Actions artifacts | 每日 03:00 | SQL 使用者 |
| 指標快照 | logs/indicators/<date>.json |
每日 | 儀表板、監控 |
這張表是「Day 41-44 共用部署」的視覺化:同一個專案對外有多個入口,每個入口服務不同的使用者,但底層都讀同一份 DuckDB 與同一份 project_config.py。Day 41 寫的合約(aqi_hourly.yml)與共用設定(project_config.py)在四天內被十幾支程式碼引用,這是「共用設定單一來源」的具體價值——任何指標、模型、排程的變更都集中在一個地方,整個管線因此保持一致。
結語
今天的重點是「讓管線被看到、被使用」。我們從 DuckDB 的 artifact 上傳開始,到 Streamlit 儀表板、文件站、CI 流程,把整個專案從「能跑」推進到「能展示」。讀完這篇你應該能回答:為什麼 DuckDB 要做成 artifact 而不是 commit?Streamlit 儀表板怎麼讀 DuckDB?CI 流程該跑哪些檢查?明天(Day 45),我們會做整個系列的最後一篇「系列總結與延伸路線」,把 45 天的工具、觀念、專案實作串成一張地圖。
延伸資源
- Streamlit Community Cloud 官方文件(2025):
https://docs.streamlit.io/streamlit-community-cloud。免費部署 Streamlit 應用的入口。 - GitHub Actions Artifact 官方文件:
https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts。把 build 產出物儲存到 GitHub 的標準機制。 - MkDocs gh-deploy 官方文件:
https://www.mkdocs.org/user-guide/deploying-your-docs/。mkdocs gh-deploy推到 GitHub Pages 的標準做法。 - peaceiris/actions-gh-pages 官方 repo:
https://github.com/peaceiris/actions-gh-pages。GitHub Pages 部署的最熱門 action。 - Day 40 章節(文件化與交接):今天部署的 MkDocs 文件站是 Day 40 的延伸,把文件公開給所有人查。
- Day 42 章節(管線實作與排程):今天部署的 GitHub Actions workflow 是 Day 42 的延伸,把 daily_run 推到雲端。
留言
張貼留言