核心目标:把本地 uv 工作流可靠迁移到 GitHub Actions 和生产容器,正确使用锁文件、缓存、私有索引和短期凭据,建立可审计的交付链路。
前置知识:已掌握 uv 项目、锁文件、构建发布和 workspace。
文档基线:uv 0.11.x;GitHub Actions、Docker 和索引行为依据 2026-07-21 的 uv 官方文档复核。CI 中应定期升级并重新验证固定版本。
7.1 生产交付的四条底线
本地执行成功不代表能够稳定交付。CI 和容器至少满足:
- 输入可追踪:代码提交、Python、uv、锁文件和基础镜像都有明确版本;
- 构建可重复 :CI 不静默改锁文件,容器不复制本机
.venv; - 权限最小化:测试 job 没有发布权限,长期 token 不进入镜像和日志;
- 缓存可丢弃:删除全部缓存后仍能得到正确结果。
#mermaid-svg-JebYDxhyCXl6pNhP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JebYDxhyCXl6pNhP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JebYDxhyCXl6pNhP .error-icon{fill:#552222;}#mermaid-svg-JebYDxhyCXl6pNhP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JebYDxhyCXl6pNhP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JebYDxhyCXl6pNhP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JebYDxhyCXl6pNhP .marker.cross{stroke:#333333;}#mermaid-svg-JebYDxhyCXl6pNhP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JebYDxhyCXl6pNhP p{margin:0;}#mermaid-svg-JebYDxhyCXl6pNhP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JebYDxhyCXl6pNhP .cluster-label text{fill:#333;}#mermaid-svg-JebYDxhyCXl6pNhP .cluster-label span{color:#333;}#mermaid-svg-JebYDxhyCXl6pNhP .cluster-label span p{background-color:transparent;}#mermaid-svg-JebYDxhyCXl6pNhP .label text,#mermaid-svg-JebYDxhyCXl6pNhP span{fill:#333;color:#333;}#mermaid-svg-JebYDxhyCXl6pNhP .node rect,#mermaid-svg-JebYDxhyCXl6pNhP .node circle,#mermaid-svg-JebYDxhyCXl6pNhP .node ellipse,#mermaid-svg-JebYDxhyCXl6pNhP .node polygon,#mermaid-svg-JebYDxhyCXl6pNhP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JebYDxhyCXl6pNhP .rough-node .label text,#mermaid-svg-JebYDxhyCXl6pNhP .node .label text,#mermaid-svg-JebYDxhyCXl6pNhP .image-shape .label,#mermaid-svg-JebYDxhyCXl6pNhP .icon-shape .label{text-anchor:middle;}#mermaid-svg-JebYDxhyCXl6pNhP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JebYDxhyCXl6pNhP .rough-node .label,#mermaid-svg-JebYDxhyCXl6pNhP .node .label,#mermaid-svg-JebYDxhyCXl6pNhP .image-shape .label,#mermaid-svg-JebYDxhyCXl6pNhP .icon-shape .label{text-align:center;}#mermaid-svg-JebYDxhyCXl6pNhP .node.clickable{cursor:pointer;}#mermaid-svg-JebYDxhyCXl6pNhP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JebYDxhyCXl6pNhP .arrowheadPath{fill:#333333;}#mermaid-svg-JebYDxhyCXl6pNhP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JebYDxhyCXl6pNhP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JebYDxhyCXl6pNhP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JebYDxhyCXl6pNhP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JebYDxhyCXl6pNhP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JebYDxhyCXl6pNhP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JebYDxhyCXl6pNhP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JebYDxhyCXl6pNhP .cluster text{fill:#333;}#mermaid-svg-JebYDxhyCXl6pNhP .cluster span{color:#333;}#mermaid-svg-JebYDxhyCXl6pNhP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JebYDxhyCXl6pNhP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JebYDxhyCXl6pNhP rect.text{fill:none;stroke-width:0;}#mermaid-svg-JebYDxhyCXl6pNhP .icon-shape,#mermaid-svg-JebYDxhyCXl6pNhP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JebYDxhyCXl6pNhP .icon-shape p,#mermaid-svg-JebYDxhyCXl6pNhP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JebYDxhyCXl6pNhP .icon-shape .label rect,#mermaid-svg-JebYDxhyCXl6pNhP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JebYDxhyCXl6pNhP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JebYDxhyCXl6pNhP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JebYDxhyCXl6pNhP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 仅加速
仅加速
Git 提交
pyproject.toml + uv.lock
CI 校验
lint / type / test
构建产物
wheel / sdist / image
隔离验证
SBOM / 扫描 / 冒烟
受保护发布
OIDC / 审批
uv 缓存
缓存只是一条虚线。如果移除缓存后构建失败,问题在声明或环境,而不是"缓存配置不够好"。
7.2 GitHub Actions:最小可靠工作流
.github/workflows/ci.yml:
yaml
name: ci
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Python ${{ matrix.python-version }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.12", "3.14"]
steps:
- name: Check out source
uses: actions/checkout@v7
- name: Install uv and Python
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.30"
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Verify lockfile and sync
run: uv sync --locked --all-groups --all-extras
- name: Check formatting
run: uv run --locked ruff format --check .
- name: Lint
run: uv run --locked ruff check .
- name: Type check
run: uv run --locked mypy src
- name: Test
run: uv run --locked pytest --cov --cov-report=term-missing
- name: Minimize persistent uv cache
if: always()
run: uv cache prune --ci
7.2.1 为什么固定 uv 和 Action
version: "0.11.30" 防止 Runner 某天自动切换 uv 行为。setup-uv 固定到完整提交 SHA,降低 tag 被移动带来的供应链风险。
上例为易读仍使用 actions/checkout@v7。高安全仓库应把所有第三方 Action(包括官方 Action)固定到审核过的完整 SHA,并由 Dependabot/Renovate 提交升级 PR。
7.2.2 为什么用 --locked
CI 的职责是验证仓库状态,不是替开发者生成新锁文件。若 pyproject.toml 与 uv.lock 不一致,--locked 应立即失败:
powershell
uv lock
git diff -- pyproject.toml uv.lock
在本地解决并提交,而不是在 workflow 中执行普通 uv lock 后继续。
7.2.3 Python 矩阵如何选择
库项目至少测试:
requires-python的最低支持版本;- 团队默认版本;
- 当前稳定 Python。
应用项目可以只测试实际部署版本,再额外增加升级预演。矩阵中的 3.14 是本文时点示例;复制工作流时应按项目真实支持范围调整。
7.3 拆分快速检查与完整矩阵
在每个 OS/Python 组合重复 Ruff 和 Mypy 往往没有收益。大型项目可拆为:
#mermaid-svg-Za1mF98VXmes4sff{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Za1mF98VXmes4sff .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Za1mF98VXmes4sff .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Za1mF98VXmes4sff .error-icon{fill:#552222;}#mermaid-svg-Za1mF98VXmes4sff .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Za1mF98VXmes4sff .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Za1mF98VXmes4sff .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Za1mF98VXmes4sff .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Za1mF98VXmes4sff .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Za1mF98VXmes4sff .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Za1mF98VXmes4sff .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Za1mF98VXmes4sff .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Za1mF98VXmes4sff .marker.cross{stroke:#333333;}#mermaid-svg-Za1mF98VXmes4sff svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Za1mF98VXmes4sff p{margin:0;}#mermaid-svg-Za1mF98VXmes4sff .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Za1mF98VXmes4sff .cluster-label text{fill:#333;}#mermaid-svg-Za1mF98VXmes4sff .cluster-label span{color:#333;}#mermaid-svg-Za1mF98VXmes4sff .cluster-label span p{background-color:transparent;}#mermaid-svg-Za1mF98VXmes4sff .label text,#mermaid-svg-Za1mF98VXmes4sff span{fill:#333;color:#333;}#mermaid-svg-Za1mF98VXmes4sff .node rect,#mermaid-svg-Za1mF98VXmes4sff .node circle,#mermaid-svg-Za1mF98VXmes4sff .node ellipse,#mermaid-svg-Za1mF98VXmes4sff .node polygon,#mermaid-svg-Za1mF98VXmes4sff .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Za1mF98VXmes4sff .rough-node .label text,#mermaid-svg-Za1mF98VXmes4sff .node .label text,#mermaid-svg-Za1mF98VXmes4sff .image-shape .label,#mermaid-svg-Za1mF98VXmes4sff .icon-shape .label{text-anchor:middle;}#mermaid-svg-Za1mF98VXmes4sff .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Za1mF98VXmes4sff .rough-node .label,#mermaid-svg-Za1mF98VXmes4sff .node .label,#mermaid-svg-Za1mF98VXmes4sff .image-shape .label,#mermaid-svg-Za1mF98VXmes4sff .icon-shape .label{text-align:center;}#mermaid-svg-Za1mF98VXmes4sff .node.clickable{cursor:pointer;}#mermaid-svg-Za1mF98VXmes4sff .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Za1mF98VXmes4sff .arrowheadPath{fill:#333333;}#mermaid-svg-Za1mF98VXmes4sff .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Za1mF98VXmes4sff .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Za1mF98VXmes4sff .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Za1mF98VXmes4sff .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Za1mF98VXmes4sff .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Za1mF98VXmes4sff .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Za1mF98VXmes4sff .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Za1mF98VXmes4sff .cluster text{fill:#333;}#mermaid-svg-Za1mF98VXmes4sff .cluster span{color:#333;}#mermaid-svg-Za1mF98VXmes4sff div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Za1mF98VXmes4sff .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Za1mF98VXmes4sff rect.text{fill:none;stroke-width:0;}#mermaid-svg-Za1mF98VXmes4sff .icon-shape,#mermaid-svg-Za1mF98VXmes4sff .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Za1mF98VXmes4sff .icon-shape p,#mermaid-svg-Za1mF98VXmes4sff .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Za1mF98VXmes4sff .icon-shape .label rect,#mermaid-svg-Za1mF98VXmes4sff .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Za1mF98VXmes4sff .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Za1mF98VXmes4sff .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Za1mF98VXmes4sff :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Pull Request
快速检查
Ruff + Mypy + lock check
测试矩阵
Python × OS
合并门禁
main/tag 构建
受保护发布
快速 job:
yaml
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
version: "0.11.30"
enable-cache: true
- run: uv lock --check
- run: uv sync --locked --all-groups
- run: uv run ruff format --check .
- run: uv run ruff check .
- run: uv run mypy src
矩阵 job 只执行必要测试。最终分支保护同时要求两个 job 通过。
7.4 Workspace 的 CI
最可靠的基线是全量安装和测试:
yaml
- name: Sync all workspace members
run: uv sync --locked --all-packages --all-groups
- name: Test all members
run: uv run --all-packages pytest
- name: Build all publishable members
run: uv build --all-packages --clear --no-sources
为防止共享环境掩盖未声明依赖,还应为重要成员增加隔离 job:
yaml
- name: Test weather-core as a package target
run: |
uv sync --locked --package weather-core
uv run --package weather-core pytest packages/weather-core/tests
大仓库按变更范围优化时,必须包含反向依赖。核心库变化不能只测试核心库自身。
7.5 正确缓存 uv
setup-uv 内置缓存:
yaml
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
version: "0.11.30"
enable-cache: true
cache-dependency-glob: "uv.lock"
7.5.1 缓存键包含什么
通常至少包含:
- 操作系统和架构;
- uv 缓存格式相关信息;
uv.lock哈希;- 必要时包含 Python 版本或构建工具输入。
不要缓存整个 .venv 作为跨 Runner 复用策略。虚拟环境含绝对路径、解释器引用和平台二进制;缓存 uv 下载/构建产物,再用锁文件快速重建环境更稳妥。
7.5.2 uv cache prune --ci
CI 结束时:
powershell
uv cache prune --ci
它针对 CI 缓存保留更值得复用的本地构建 wheel,清理可快速重新下载的预构建 wheel 和展开的源码分发物。是否能加速取决于项目依赖,不应脱离测量机械添加。
7.5.3 Self-hosted Runner
自托管 Runner 的缓存不会随 job 销毁,可能无限增长。应:
- 为 Runner 配置明确
UV_CACHE_DIR; - 定期执行
uv cache prune; - 监控磁盘和 inode;
- 不让不同信任级别的仓库共享可写缓存;
- 严禁手工修改缓存内部文件。
7.6 发布 Job 的权限隔离
测试 job 不需要 id-token: write。发布 job 应独立,并依赖构建验证:
yaml
publish:
if: startsWith(github.ref, 'refs/tags/v')
needs: [quality, test]
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
version: "0.11.30"
- run: uv build --clear --no-sources
- run: uv publish --trusted-publishing always
进一步改进:
- 受保护 environment 需要审批;
- tag 版本必须等于
pyproject.toml版本; - 构建一次,验证后发布同一组不可变产物;
- 不在发布 job 临时修改版本或锁文件;
- 使用 OIDC Trusted Publishing,避免长期 PyPI token。
7.7 Docker 中安装 uv
官方提供仅包含 uv 二进制的 distroless 镜像。常见做法:
dockerfile
FROM python:3.12-slim-trixie
COPY --from=ghcr.io/astral-sh/uv:0.11.30 /uv /uvx /bin/
生产环境优先固定镜像 digest:
dockerfile
COPY --from=ghcr.io/astral-sh/uv@sha256:<审核过的摘要> /uv /uvx /bin/
不要复制文档中的示例摘要后长期不更新;应从组织信任的镜像仓库获取、验证并由自动化升级。
7.8 单包项目的生产 Dockerfile
dockerfile
# syntax=docker/dockerfile:1.7
ARG PYTHON_IMAGE=python:3.12-slim-trixie
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.30
FROM ${UV_IMAGE} AS uv-bin
FROM ${PYTHON_IMAGE} AS builder
COPY --from=uv-bin /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy
WORKDIR /app
# 依赖层:源码变化不会使其失效
COPY pyproject.toml uv.lock README.md ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-install-project
# 项目层
COPY src ./src
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-editable
FROM ${PYTHON_IMAGE} AS runtime
RUN groupadd --system app \
&& useradd --system --gid app --home-dir /app app
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
USER app
ENTRYPOINT ["weather"]
CMD ["Shanghai"]
7.8.1 为什么分两次同步
第一次只复制依赖声明和锁文件:
dockerfile
uv sync --locked --no-dev --no-install-project
它安装传递依赖,不安装频繁变化的当前项目。源码复制后第二次同步安装项目。业务代码变化不会使整个依赖层失效。
7.8.2 为什么使用 UV_LINK_MODE=copy
BuildKit cache mount 与目标 .venv 可能位于不同文件系统,硬链接不可用。设置 copy 可避免链接警告,并确保最终镜像层不依赖已卸载的 cache mount。
7.8.3 为什么最终镜像不包含 uv
运行时只需要 .venv 中的 Python 和入口脚本。将 uv 留在 builder 可缩小攻击面。若生产运维确实需要 uv run,可以复制 uv,但要明确理由。
7.9 .dockerignore 不可省略
dockerignore
.git/
.github/
.venv/
__pycache__/
.pytest_cache/
.mypy_cache/
.ruff_cache/
dist/
build/
.env
.env.*
*.pem
*.key
.venv 必须排除:本机环境不可移植,而且可能覆盖容器刚创建的 Linux 环境。敏感文件同时应从 Git 和构建上下文排除;.dockerignore 不是秘密管理系统,只是最后一道防线。
7.10 Workspace 的 Docker 分层
早期依赖层如果只看到根 pyproject.toml,uv 无法验证锁文件是否与所有成员一致。因此官方建议:
dockerfile
# syntax=docker/dockerfile:1.7
FROM python:3.12-slim-trixie AS builder
COPY --from=ghcr.io/astral-sh/uv:0.11.30 /uv /uvx /bin/
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1
WORKDIR /app
# 此阶段没有成员 pyproject.toml,跳过新鲜度检查和成员安装
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-dev --no-install-workspace
COPY . /app
# 看到完整 workspace 后必须严格校验
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-editable --package weather-cli
这里早期使用 --frozen 不是因为它更严格,而是因为缺少成员元数据,无法执行完整新鲜度检查。复制完整仓库后,最终 --locked 必须成功。
7.11 容器安全与可重复性
7.11.1 基础镜像
- 固定 Python 小版本或 digest;
- 定期重建以获取系统安全更新;
- slim 镜像可能缺编译器和共享库,构建扩展时使用 builder;
- Alpine 使用 musl,不应假设 manylinux wheel 可直接复用;
- 选择镜像前检查目标依赖是否提供对应 wheel。
7.11.2 非 root 运行
最终镜像创建专用用户并 USER app。若程序需要写目录,应明确创建并授权,而不是把整个 /app 设为 777。
7.11.3 字节码
UV_COMPILE_BYTECODE=1 可减少首次启动编译成本,但会增加构建时间和镜像体积。短生命周期 CLI 未必收益明显,Web 服务和无服务器冷启动场景应测量后决定。
7.11.4 构建秘密
私有索引凭据不要用 ARG 或 ENV 烘焙进镜像层。使用 BuildKit secret mount:
dockerfile
RUN --mount=type=secret,id=uv_index_password \
UV_INDEX_INTERNAL_PASSWORD="$(cat /run/secrets/uv_index_password)" \
uv sync --locked --no-dev
真实项目还需提供用户名或 credential provider。构建日志不得回显秘密。
7.12 私有索引配置
toml
[[tool.uv.index]]
name = "internal"
url = "https://packages.example.com/simple"
explicit = true
authenticate = "always"
[tool.uv.sources]
company-weather-sdk = { index = "internal" }
7.12.1 explicit = true
只有通过 [tool.uv.sources] 显式绑定的包才能从该索引安装。这样不会因为添加私有索引,就让所有公共依赖都从私有源搜索。
7.12.2 默认 first-index
uv 默认对一个包停在第一个包含它的索引,并只在该索引的候选版本中解析。这与 pip 常见的合并候选行为不同,目的是降低 dependency confusion 风险。
不要为了"版本更新"随意启用:
powershell
uv sync --index-strategy unsafe-best-match
它会合并多个索引候选,更接近 pip,但显著扩大同名恶意包风险。优先修复索引顺序、同步代理或显式包绑定。
7.12.3 凭据环境变量
索引名 internal 对应:
powershell
$env:UV_INDEX_INTERNAL_USERNAME = "ci-user"
$env:UV_INDEX_INTERNAL_PASSWORD = "<secret>"
uv sync --locked
名称中的非字母数字会转换为下划线并大写。例如 internal-proxy 对应 UV_INDEX_INTERNAL_PROXY_PASSWORD。
authenticate = "always" 适合那些未认证请求会被重定向到公共页面、因而不会返回标准 401 的索引;它要求 uv 在请求前主动寻找凭据。
7.13 企业网络、证书与离线环境
7.13.1 代理
使用组织标准的 HTTPS_PROXY/HTTP_PROXY 配置,并确认代理不会破坏包哈希和 TLS 验证。CI Secret 中的代理凭据同样不能打印。
7.13.2 企业 CA
优先让 Runner/容器信任组织 CA。uv 支持使用平台证书存储,但不要把 --allow-insecure-host 当作长期修复;它会降低 TLS 保护。
7.13.3 离线 wheelhouse
可把审核过的 wheel 放在 flat index:
toml
[[tool.uv.index]]
name = "offline"
url = "./wheelhouse"
format = "flat"
explicit = true
离线交付必须覆盖目标平台、Python ABI 和所有传递依赖。只在联网开发机下载一次并不等于完成离线验证。
7.14 供应链控制
7.14.1 时间冷却
toml
[tool.uv]
exclude-newer = "7 days"
冷却期能避免立即采用刚上传的发行物,为社区和安全系统留出观察时间。它会降低更新速度,安全补丁需要例外流程。
7.14.2 SBOM
powershell
uv export --format cyclonedx1.5 --output-file sbom.json
SBOM 应与具体提交、锁文件和构建产物关联。它列出组件,不自动判断漏洞是否可利用。
7.14.3 发布证明
PyPI Trusted Publishing、容器 provenance 和签名各自解决不同问题:身份、构建来源和产物完整性。生产流程应保存:
- Git commit/tag;
- CI run ID;
uv.lock哈希;- wheel/sdist/image digest;
- SBOM 和扫描结果;
- 发布环境审批记录。
7.15 常见故障
CI 本地通过但 Runner 失败
按顺序检查:
- CI Python 是否在
requires-python范围内; - 是否提交了最新
uv.lock; - 本地是否依赖未声明的全局包;
- 目标平台是否有兼容 wheel 或编译工具;
- 私有索引和凭据是否只在本机配置;
- 删除缓存后是否仍失败。
Docker 每次都重新安装依赖
确认 COPY . /app 没有发生在依赖层之前。先复制 pyproject.toml、uv.lock 和构建元数据,再执行 --no-install-project。
容器出现跨文件系统链接警告
在 BuildKit cache mount 场景设置:
dockerfile
ENV UV_LINK_MODE=copy
Workspace 早期依赖层报锁文件过期
早期层缺少成员元数据,使用 --frozen --no-install-workspace;复制完整 workspace 后必须执行 --locked。
私有包解析到了公共 PyPI
使用 explicit = true 和 [tool.uv.sources] 将包绑定到命名索引,检查索引优先级,不要用 unsafe-best-match 掩盖配置问题。
私有索引持续 401/403
检查环境变量名称转换、token 权限、索引 URL 是否以 /simple 结尾、代理和 CA;需要主动认证的索引设置 authenticate = "always"。
7.16 生产验收清单
CI
- uv、Python 和第三方 Action 版本固定且有升级流程。
-
uv sync --locked在空缓存 Runner 上成功。 - 最低支持 Python 和生产 Python 都有测试。
- 测试 job 只有只读权限,发布权限位于独立受保护 job。
- 缓存键包含锁文件,删除缓存不影响正确性。
Docker
-
.dockerignore排除.venv、Git、缓存和秘密。 - 依赖层与源码层分开。
- 最终镜像以非 root 用户运行。
- 基础镜像和 uv 镜像固定版本/digest。
- 私有索引秘密通过 secret mount 提供,不进入镜像历史。
- 从最终镜像执行健康检查或 CLI 冒烟测试。
供应链
- 内部包显式绑定私有索引。
- 保持默认
first-index,例外经过安全评审。 - 发布优先使用 OIDC,没有长期 PyPI token。
- 产物、SBOM、commit 和 CI run 能互相追踪。
- 构建产物经过漏洞、许可证和秘密扫描。
7.17 本篇小结
生产级 uv 流程的重点不是"CI 里也能运行 uv sync",而是把锁文件当作不可变输入、把缓存当作可丢弃加速层、把测试与发布权限分离,并保证 Docker 最终镜像只包含运行必需内容。私有索引的 explicit 绑定和默认 first-index 则为 Python 依赖供应链提供了重要边界。
下一篇将给出从 pip/pip-tools、Poetry、PDM 和 Pipenv 迁移的分阶段方案,并建立覆盖解释器、解析、构建、网络和缓存的系统排障方法。