diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..7666986 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,11 @@ +# GitHub Actions 配置文件 + +此目录包含 GitHub Actions 工作流配置文件。 + +## 文件说明 + +- `test.yml` - 单元测试工作流 +- `publish.yml` - PyPI 发布工作流 +- `README.md` - 配置说明文档 + +详细说明请查看 [README.md](README.md) diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..725a099 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,189 @@ +# GitHub Actions 配置说明 + +本项目使用 GitHub Actions 进行自动化测试和发布。 + +## 📋 工作流列表 + +### 1. 单元测试 (`test.yml`) + +**触发条件**: +- 推送到 `main` 分支 +- 向 `main` 分支提交 Pull Request + +**测试矩阵**: +- Python 3.9, 3.10, 3.11, 3.12, 3.13 +- Ubuntu Latest + +**执行内容**: +1. 检出代码 +2. 设置 Python 环境(多版本) +3. 安装项目和开发依赖 +4. 运行单元测试(pytest) +5. 生成覆盖率报告 +6. 上传覆盖率到 Codecov(仅 Python 3.11) + +### 2. 发布到 PyPI (`publish.yml`) + +**触发条件**: +- 推送 tag(格式:`v*.*.*`,如 `v0.3.0`) + +**执行内容**: +1. 检出代码 +2. 设置 Python 环境 +3. 构建分发包 +4. 发布到 TestPyPI(测试) +5. 发布到 PyPI(正式) +6. 创建 GitHub Release + +--- + +## 🔐 配置 Secrets + +在 GitHub 仓库中配置以下 Secrets: + +### 必需的 Secrets + +1. **`PYPI_API_TOKEN`** - PyPI API Token + - 获取地址: https://pypi.org/manage/account/token/ + - 用途: 发布到正式 PyPI + +2. **`TESTPYPI_API_TOKEN`** - TestPyPI API Token + - 获取地址: https://test.pypi.org/manage/account/token/ + - 用途: 发布到 TestPyPI 测试 + +### 可选的 Secrets + +3. **`CODECOV_TOKEN`** - Codecov Token + - 获取地址: https://codecov.io/ + - 用途: 上传测试覆盖率报告 + +### 配置步骤 + +1. 进入 GitHub 仓库 +2. Settings → Secrets and variables → Actions +3. 点击 "New repository secret" +4. 添加上述 Secrets + +--- + +## 🚀 使用方法 + +### 运行测试 + +推送代码到 `main` 分支即可自动触发测试: + +```bash +git add . +git commit -m "Update code" +git push origin main +``` + +### 发布新版本 + +1. **更新版本号** + ```bash + # 编辑 pyproject.toml + version = "0.3.1" + + # 编辑 src/debug_helpers/__init__.py + __version__ = "0.3.1" + + # 更新 CHANGELOG.md + ``` + +2. **提交更改** + ```bash + git add pyproject.toml src/debug_helpers/__init__.py CHANGELOG.md + git commit -m "Bump version to 0.3.1" + git push origin main + ``` + +3. **创建 tag 并推送** + ```bash + git tag v0.3.1 + git push origin v0.3.1 + ``` + +4. **自动发布** + - GitHub Actions 会自动触发 + - 先发布到 TestPyPI + - 再发布到 PyPI + - 创建 GitHub Release + +--- + +## 📊 查看结果 + +### 测试结果 +- 访问: `https://github.com///actions` +- 查看 "Python Unit Tests" 工作流 + +### 覆盖率报告 +- 访问: `https://codecov.io/gh//` +- 查看详细覆盖率报告 + +### 发布状态 +- 访问: `https://github.com///actions` +- 查看 "Publish to PyPI" 工作流 +- 查看 Releases 页面 + +--- + +## 🔧 本地测试 Actions + +使用 [act](https://github.com/nektos/act) 在本地测试 GitHub Actions: + +```bash +# 安装 act +brew install act # macOS +# 或 +curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash + +# 测试 test.yml +act push + +# 测试 publish.yml +act push --eventpath .github/workflows/event.json +``` + +--- + +## 📝 注意事项 + +### 测试工作流 +- ✅ 测试多个 Python 版本以确保兼容性 +- ✅ 使用 `fail-fast: false` 确保所有版本都被测试 +- ✅ 仅在 Python 3.11 上传覆盖率报告(避免重复) + +### 发布工作流 +- ⚠️ 发布是不可逆的操作 +- ⚠️ 确保版本号在 `pyproject.toml` 和 `__init__.py` 中保持一致 +- ⚠️ 相同版本号无法重新上传到 PyPI +- ✅ 先发布到 TestPyPI 可以预先测试 +- ✅ 使用 `--skip-existing` 避免 TestPyPI 重复上传错误 + +### Token 安全 +- 🔒 永远不要在代码中硬编码 Token +- 🔒 使用 GitHub Secrets 管理敏感信息 +- 🔒 Token 应设置合适的权限范围 + +--- + +## 🎯 工作流状态徽章 + +在 README.md 中添加徽章: + +```markdown +![Tests](https://github.com///workflows/Python%20Unit%20Tests/badge.svg) +![PyPI](https://img.shields.io/pypi/v/debug-helpers.svg) +![Coverage](https://codecov.io/gh///branch/main/graph/badge.svg) +``` + +--- + +## 🔗 相关链接 + +- [GitHub Actions 文档](https://docs.github.com/en/actions) +- [Python GitHub Actions](https://github.com/actions/setup-python) +- [PyPI Publishing with GitHub Actions](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/) +- [Codecov](https://codecov.io/) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..15c97fa --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,55 @@ +# 发布到 PyPI - 创建新 tag 时自动触发 +name: Publish to PyPI + +on: + push: + tags: + - 'v*.*.*' # 匹配 v0.1.0, v1.2.3 等格式的 tag + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 设置 Python 环境 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: 安装构建工具 + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: 构建分发包 + run: | + python -m build + + - name: 检查分发包 + run: | + twine check dist/* + + - name: 发布到 TestPyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TESTPYPI_API_TOKEN }} + run: | + twine upload --repository testpypi dist/* --skip-existing + + - name: 发布到 PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + twine upload dist/* + + - name: 创建 GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: dist/* + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..63cb34f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,56 @@ +# Python 项目单元测试 - 推送到 main 分支时自动运行 +name: Python Unit Tests + +on: + push: + branches: [main] # 仅在推送到 main 分支时触发 + pull_request: + branches: [main] # 也可在 PR 时运行 + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + fail-fast: false # 一个版本失败不影响其他版本继续测试 + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 设置 Python ${{ matrix.python-version }} 环境 + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' # 自动缓存 pip 依赖 + + - name: 显示 Python 版本 + run: | + python --version + pip --version + + - name: 升级 pip + run: | + python -m pip install --upgrade pip + + - name: 安装项目和开发依赖 + run: | + pip install -e '.[dev]' + + - name: 显示已安装的包 + run: | + pip list + + - name: 运行单元测试 + run: | + pytest tests/ -v --cov=src/debug_helpers --cov-report=term-missing --cov-report=xml + + - name: 上传覆盖率报告到 Codecov + if: success() && matrix.python-version == '3.11' + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + fail_ci_if_error: false + verbose: true diff --git a/Makefile b/Makefile index dcbd2f4..e9dac6f 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,17 @@ uninstall: # 运行单元测试 test: @echo "==> 运行单元测试..." - pytest tests/ -v + @echo "" + @if command -v pytest >/dev/null 2>&1; then \ + echo "使用 pytest 运行测试..."; \ + pytest tests/ -v; \ + else \ + echo "使用 unittest 运行测试..."; \ + python -m unittest discover -s tests -v; \ + fi + @echo "" + @echo "💡 提示: 安装 pytest 可以获得更好的测试体验" + @echo " pip install -e '.[dev]'" # 运行示例代码 example: diff --git a/docs/03_github_actions_explained.md b/docs/03_github_actions_explained.md new file mode 100644 index 0000000..5524714 --- /dev/null +++ b/docs/03_github_actions_explained.md @@ -0,0 +1,357 @@ +# GitHub Actions 工作原理详解 + +## 🔍 test.yml 工作原理 + +### 1. 触发机制 + +```yaml +on: + push: + branches: [main] # 推送到 main 分支时触发 + pull_request: + branches: [main] # PR 到 main 分支时触发 +``` + +**工作流程**: +1. 你执行 `git push origin main` +2. GitHub 检测到 push 事件 +3. GitHub 检查仓库中的 `.github/workflows/*.yml` 文件 +4. 找到匹配的工作流(`on.push.branches: [main]`) +5. 自动启动工作流执行 + +### 2. 运行环境 + +```yaml +runs-on: ubuntu-latest +``` + +**GitHub 提供的免费虚拟机**: +- **操作系统**: Ubuntu 最新版(目前是 Ubuntu 22.04) +- **虚拟环境**: 全新的 VM,每次运行都是干净的 +- **配置**: 2-core CPU, 7GB RAM, 14GB SSD +- **时长限制**: 单个 job 最多 6 小时 +- **位置**: GitHub 的云端服务器(Azure) + +### 3. 测试矩阵 + +```yaml +strategy: + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + fail-fast: false +``` + +**并行执行**: +- GitHub 会创建 **5 个独立的虚拟机** +- 每个 VM 运行一个 Python 版本 +- **并行运行**,不是串行 +- `fail-fast: false` 表示一个失败不影响其他的继续 + +**示意图**: +``` +GitHub 检测到 push + ↓ +启动 5 个并行任务 + ├─ VM1: Python 3.9 ─┐ + ├─ VM2: Python 3.10 ─┤ + ├─ VM3: Python 3.11 ─┼─ 同时运行 + ├─ VM4: Python 3.12 ─┤ + └─ VM5: Python 3.13 ─┘ + ↓ + 所有任务完成 + ↓ + 显示结果(✅ 或 ❌) +``` + +### 4. 执行步骤详解 + +#### Step 1: 检出代码 +```yaml +- name: 检出代码 + uses: actions/checkout@v4 +``` +- **作用**: 将你的仓库代码克隆到虚拟机 +- **原理**: 类似 `git clone` +- **版本**: `@v4` 是 action 的版本 + +#### Step 2: 设置 Python +```yaml +- name: 设置 Python ${{ matrix.python-version }} 环境 + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' +``` +- **作用**: 安装指定版本的 Python +- **cache: 'pip'**: 自动缓存 pip 下载的包,下次运行更快 +- **`${{ matrix.python-version }}`**: 变量替换,根据矩阵值动态设置 + +#### Step 3-4: 显示版本和升级 pip +```yaml +- name: 显示 Python 版本 + run: | + python --version + pip --version + +- name: 升级 pip + run: | + python -m pip install --upgrade pip +``` +- **`run:`**: 执行 shell 命令 +- **`|`**: YAML 多行字符串 + +#### Step 5: 安装依赖 +```yaml +- name: 安装项目和开发依赖 + run: | + pip install -e '.[dev]' +``` +- **原理**: + 1. 读取 `pyproject.toml` + 2. 安装项目本身(`-e` 开发模式) + 3. 安装 `[project.optional-dependencies]` 下的 `dev` 依赖 + 4. 包括 `pytest` 和 `pytest-cov` + +#### Step 6: 运行测试 +```yaml +- name: 运行单元测试 + run: | + pytest tests/ -v --cov=src/debug_helpers --cov-report=term-missing --cov-report=xml +``` +- **pytest tests/**: 运行测试 +- **--cov=src/debug_helpers**: 生成覆盖率 +- **--cov-report=xml**: 生成 XML 格式报告(给 Codecov 用) + +#### Step 7: 上传覆盖率(可选) +```yaml +- name: 上传覆盖率报告到 Codecov + if: success() && matrix.python-version == '3.11' + uses: codecov/codecov-action@v4 +``` +- **`if:`**: 条件执行 +- **`success()`**: 前面步骤都成功 +- **只在 Python 3.11 上传**: 避免重复上传 + +--- + +## ⚙️ 需要在 GitHub 上设置什么? + +### 必需设置(零配置) + +✅ **无需任何设置!** + +只需要: +1. 将 `.github/workflows/test.yml` 推送到仓库 +2. GitHub 会自动识别并启用 + +### 可选设置 + +#### 1. 查看工作流状态 + +在仓库页面: +- 点击 **"Actions"** 标签 +- 可以看到所有工作流的运行历史 +- 点击某次运行可以查看详细日志 + +#### 2. 配置 Codecov(可选) + +如果要使用覆盖率报告: + +1. 访问 https://codecov.io/ +2. 用 GitHub 账号登录 +3. 添加你的仓库 +4. 获取 `CODECOV_TOKEN` +5. 在 GitHub 仓库添加 Secret: + - Settings → Secrets and variables → Actions + - New repository secret + - Name: `CODECOV_TOKEN` + - Value: 粘贴 token + +**如果不设置**: 覆盖率步骤会被跳过,测试仍然正常运行 + +#### 3. 保护分支(推荐) + +Settings → Branches → Add branch protection rule: +- Branch name pattern: `main` +- ✅ Require status checks to pass before merging +- ✅ Require branches to be up to date before merging +- 选择: `test` (Python Unit Tests) + +**效果**: PR 必须通过测试才能合并 + +--- + +## 💰 资源和费用 + +### 免费额度(Public 仓库) + +| 项目 | 额度 | +|------|------| +| **运行时长** | ♾️ **无限制** | +| **并发任务** | 20 个 | +| **存储空间** | 500 MB | +| **成本** | 完全免费 | + +### 免费额度(Private 仓库) + +| 项目 | 免费额度 | 超出后价格 | +|------|----------|-----------| +| **运行时长** | 2,000 分钟/月 | $0.008/分钟 | +| **并发任务** | 20 个 | - | +| **存储空间** | 500 MB | $0.25/GB/月 | + +### 本项目的资源消耗 + +以 **test.yml** 为例: + +``` +单次运行: + - 5 个 Python 版本并行 + - 每个版本约 2-3 分钟 + - 并行运行,实际耗时: ~3 分钟 + - 计费时长: 5 × 3 = 15 分钟 + +每月推送 20 次: + - 总耗时: 20 × 15 = 300 分钟 + - Public 仓库: 免费 + - Private 仓库: 300/2000,仍在免费额度内 +``` + +### GitHub Actions 运行器 + +**GitHub 托管的运行器(Runners)**: + +| 类型 | 配置 | 位置 | +|------|------|------| +| `ubuntu-latest` | 2-core, 7GB RAM | Azure 云端 | +| `ubuntu-22.04` | 2-core, 7GB RAM | Azure 云端 | +| `windows-latest` | 2-core, 7GB RAM | Azure 云端 | +| `macos-latest` | 3-core, 14GB RAM | Azure 云端 | + +**自托管运行器**(可选): +- 你也可以使用自己的服务器 +- 但对于简单项目,GitHub 托管的完全够用 + +--- + +## 🔄 完整工作流程图 + +``` +开发者操作 + | + | git push origin main + ↓ +GitHub 服务器检测到 push 事件 + | + | 读取 .github/workflows/test.yml + ↓ +GitHub Actions 调度器 + | + | 分配 5 个虚拟机(并行) + ↓ +┌─────────────────────────────────────────┐ +│ VM1 (Python 3.9) │ +│ ├─ 检出代码 │ +│ ├─ 安装 Python 3.9 │ +│ ├─ 安装依赖 │ +│ ├─ 运行测试 │ +│ └─ ✅ 成功 / ❌ 失败 │ +└─────────────────────────────────────────┘ +┌─────────────────────────────────────────┐ +│ VM2 (Python 3.10) │ +│ ... 同样的步骤 ... │ +└─────────────────────────────────────────┘ +┌─────────────────────────────────────────┐ +│ VM3 (Python 3.11) │ +│ ... 同样的步骤 ... │ +│ └─ 额外:上传覆盖率到 Codecov │ +└─────────────────────────────────────────┘ +┌─────────────────────────────────────────┐ +│ VM4 (Python 3.12) │ +│ ... 同样的步骤 ... │ +└─────────────────────────────────────────┘ +┌─────────────────────────────────────────┐ +│ VM5 (Python 3.13) │ +│ ... 同样的步骤 ... │ +└─────────────────────────────────────────┘ + | + | 所有任务完成 + ↓ +GitHub 汇总结果 + | + ├─ 发送邮件通知(如果失败) + ├─ 在 PR 中显示状态 + └─ Actions 页面显示详细日志 +``` + +--- + +## 📊 实际运行示例 + +### 成功的运行 + +``` +✅ Python Unit Tests #42 + Triggered by: push to main + Duration: 3m 24s + + Jobs: + ✅ test (3.9) - 2m 45s + ✅ test (3.10) - 2m 52s + ✅ test (3.11) - 3m 08s + ✅ test (3.12) - 2m 58s + ✅ test (3.13) - 3m 02s +``` + +### 失败的运行 + +``` +❌ Python Unit Tests #43 + Triggered by: push to main + Duration: 2m 15s + + Jobs: + ✅ test (3.9) - 2m 10s + ❌ test (3.10) - 1m 45s ← 失败 + ✅ test (3.11) - 2m 05s + ✅ test (3.12) - 1m 58s + ✅ test (3.13) - 2m 00s + + Error in test (3.10): + > FAILED tests/test_example.py::test_add - AssertionError +``` + +--- + +## 🎯 总结 + +### 核心要点 + +1. **完全自动化**: 推送代码即可,无需手动触发 +2. **零配置**: 只需添加 YAML 文件,GitHub 自动识别 +3. **免费使用**: Public 仓库完全免费 +4. **独立环境**: 每次运行都是全新的 VM +5. **并行执行**: 多个 Python 版本同时测试 +6. **云端资源**: 使用 GitHub/Azure 的服务器 + +### 你需要做的 + +✅ **必需**: +1. 创建 `.github/workflows/test.yml` 文件 +2. 推送到 GitHub + +❌ **不需要**: +1. ❌ 不需要自己的服务器 +2. ❌ 不需要额外的配置 +3. ❌ 不需要支付费用(Public 仓库) +4. ❌ 不需要安装任何软件 + +就这么简单!🎉 + +--- + +## 📚 延伸阅读 + +- [GitHub Actions 官方文档](https://docs.github.com/en/actions) +- [工作流语法](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions) +- [GitHub Actions 计费](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions) diff --git a/docs/06_version_0.3.0.md b/docs/03_version_0.3.0.md similarity index 100% rename from docs/06_version_0.3.0.md rename to docs/03_version_0.3.0.md diff --git a/pyproject.toml b/pyproject.toml index 7e7d244..590cc6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,12 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-cov>=4.0.0", +] + [project.urls] Homepage = "https://github.com/jssfy/python_debug_helpers" Repository = "https://github.com/jssfy/python_debug_helpers" diff --git a/tests/test_example.py b/tests/test_example.py index 7f83787..8c5c90e 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -1,35 +1,53 @@ """示例测试""" -from example_package import hello, add, print_dict +import unittest +from debug_helpers import hello, add, print_dict -def test_hello() -> None: - """测试 hello 函数""" - assert hello("World") == "Hello, World!" - assert hello("Python") == "Hello, Python!" -def test_add() -> None: - """测试 add 函数""" - assert add(1, 2) == 3 - assert add(0, 0) == 0 - assert add(-1, 1) == 0 - -def test_print_dict() -> None: - """测试 print_dict 函数""" - # 测试基本字典 - data = {"name": "test", "value": 123} - # 不应该抛出异常 - print_dict(data) +class TestDebugHelpers(unittest.TestCase): + """debug_helpers 测试类""" + + def test_hello(self) -> None: + """测试 hello 函数""" + self.assertEqual(hello("World"), "Hello, World!") + self.assertEqual(hello("Python"), "Hello, Python!") + + def test_add(self) -> None: + """测试 add 函数""" + self.assertEqual(add(1, 2), 3) + self.assertEqual(add(0, 0), 0) + self.assertEqual(add(-1, 1), 0) - # 测试嵌套字典 - nested_data = { - "level1": { - "level2": { - "value": "nested" + def test_print_dict(self) -> None: + """测试 print_dict 函数""" + # 测试基本字典 + data = {"name": "test", "value": 123} + # 不应该抛出异常 + try: + print_dict(data) + except Exception as e: + self.fail(f"print_dict raised {e} unexpectedly!") + + # 测试嵌套字典 + nested_data = { + "level1": { + "level2": { + "value": "nested" + } } } - } - print_dict(nested_data) - - # 测试列表 - list_data = [1, 2, 3, {"key": "value"}] - print_dict(list_data) + try: + print_dict(nested_data) + except Exception as e: + self.fail(f"print_dict raised {e} unexpectedly!") + + # 测试列表 + list_data = [1, 2, 3, {"key": "value"}] + try: + print_dict(list_data) + except Exception as e: + self.fail(f"print_dict raised {e} unexpectedly!") + + +if __name__ == '__main__': + unittest.main()