Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ git checkout -b <your_feature_branch>
```bash
# 自动打通虚拟环境并注满燃油(安装源码及开发依赖)
uv venv
uv pip install -r requirements-dev.txt -e ./
uv pip install -r .github/requirements-dev.txt -e ./
```

### 2. 空中改造 (Coding)
Expand Down Expand Up @@ -62,30 +62,49 @@ uv run python -m unittest

- 📘 **`docs` 航道 (文档维护)**:本航道直通项目专属的 **[在线文档 (Read the Docs)](https://jmcomic.readthedocs.io/zh-cn/latest/)**。所有不涉及功能性代码修改的**纯粹文档更新**,请直接提交至此分支。入线后它将即刻触发并全网同步部署,绕过复杂的流水安检。需要注意的是,`docs` 航道的进度通常会超前于主系统 (`master`),塔台会在新版发布或定期检修时统一将二者对齐同步。

- 🚫 **禁飞区 (禁止直飞 master)**:为防止外部航班意外触发 `release_auto.yml` 这个威力巨大的自动发版工作流,**本项目不接受任何直接指向 `master` 分支的 PR**。所有的新功能与代码改造必须经由 `dev` 航道降落并完成试飞,新版本号的最终敲定与发布由塔台统一操控。
- 🚫 **禁飞区 (普通 PR 禁止直飞 master)**:为防止外部航班意外触发 `release_auto.yml` 这个威力巨大的自动发版工作流,所有的功能、重构、修复、文档和普通维护 PR 都不得直接指向 `master`。这些改动必须先经由 `dev` 或 `docs` 航道降落,完成对应试飞后再由塔台统一调度。

- 🚀 **发版专线 (仅限版本发布)**:`master` 只接收由塔台(维护者)从 `dev` 发起的正式发版 PR。该 PR 必须同时完成版本号更新,并在 `CHANGELOG.md` 中准备好对应版本的带日期发布记录;任意一项缺失,都不得指向或合并到 `master`。

PR 申请后,我会尽快 review 各位机长的代码并反馈通讯。再次感谢你的付出!🎉

---

## 🚀 发版巡航 (Release Process)

项目使用自动化流水线进行发版,此流程仅由塔台(维护者)在合并至 `master` 分支时触发
项目使用自动化流水线进行发版,此流程仅由塔台(维护者)通过 `dev → master` 发版专线触发

### 1. 触发条件
当代码被推送(或 PR 合并)至 `master` 分支,且 **Commit Message 以 `v` 开头**时,GitHub Actions 会自动启动 `release_auto.yml` 工作流,执行以下操作:
1. 自动根据 Commit Message 创建 GitHub Release 标签。
2. 自动构建项目并发布至 [PyPI](https://pypi.org/project/jmcomic/)。
1. 自动根据 Commit Message 中的版本号创建 GitHub Release 标签。
2. 从 `CHANGELOG.md` 对应版本段生成 GitHub Release 正文。
3. 在 Actions 日志中输出目标版本、Changelog 条目数量和正文来源,供塔台在正式发布前复核。
4. 自动构建项目并发布至 [PyPI](https://pypi.org/project/jmcomic/)。

### 2. Commit 格式指令 (仅针对维护者)
当塔台(维护者)准备好发布新版本并合入 `master` 时,需要根据 `.github/release.py` 的解析要求使用特定格式的 Commit Message:
当塔台(维护者)准备好发布新版本并合入 `master` 时,必须先完成以下发版检查:

1. 更新 `src/jmcomic/__init__.py` 中的 `__version__`。
2. 在 `CHANGELOG.md` 中添加同版本的 `## [版本号] - YYYY-MM-DD` 段落,并确认自上一个版本以来的重要改动均已记录。
3. 确认版本号与 Changelog 均已包含在发版 PR 中,再使用以下 Commit Message 合并到 `master`。

**格式:** `v<版本号>: <更新项1>; <更新项2>; ...`
**格式:** `v<版本号>: <简短发布说明>`

* **示例:** `v2.1.0: 修复搜索解析异常; 优化多线程下载效率; 新增插件系统`
* **示例:** `v2.7.4: 发布下载清单功能`
* **黑匣子解析逻辑:**
* **Tag**:冒号 `:` 前的内容(如 `v2.1.0`)。
* **Body**:冒号 `:` 后的内容,程序会将分号 `;` 分割的每一项转化为有序列表。
* **版本校验**:Commit Message 中的版本必须与 `jmcomic.__version__` 一致。
* **Body**:GitHub Release 正文完全来自 `CHANGELOG.md` 的同版本段落;冒号后的文字只用于说明本次发版提交,不再充当 Release 正文。

### 3. 备降与人工复飞

如果符合规范的发版提交已经进入 `master`,但自动流水线因为 Changelog 遗漏、格式错误或版本不匹配而在创建 Tag、GitHub Release 和 PyPI 包之前中止,塔台无需再制造第二条 `v版本号:` 发版提交:

1. 在 `master` 补充一条普通修正提交,只修正当前版本的版本号或 Changelog 发版资料,不夹带新的功能代码。
2. 在 GitHub Actions 中选择 `master` 分支,手动运行 `Manual Release & Publish`。
3. 手动流水线会直接读取 `src/jmcomic/__init__.py` 中的版本号,并使用 `CHANGELOG.md` 的同版本段落完成发布。

人工复飞仅用于修复自动发版的前置校验失败,不替代正常的 `dev → master` 发版专线,也不得用于绕过发版 PR 的版本与 Changelog 检查。

---

Expand Down
117 changes: 88 additions & 29 deletions .github/release.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,104 @@
"""Build GitHub Release metadata from a release commit and changelog."""

import ast
import os
import sys
import re
import sys
from pathlib import Path
from typing import Optional, Tuple


def add_output(k, v):
cmd = f'echo "{k}={v}" >> $GITHUB_OUTPUT'
print(cmd, os.system(cmd))

ROOT_DIR = Path(__file__).resolve().parent.parent
VERSION_FILE = Path("src/jmcomic/__init__.py")
CHANGELOG_FILE = Path("CHANGELOG.md")
RELEASE_BODY_FILE = Path("release_body.txt")
RELEASE_SUBJECT_PATTERN = re.compile(r"^v(?P<version>\d+\.\d+\.\d+):(?:\s.*)?$")
VERSION_HEADING_PATTERN = re.compile(
r"^## \[(?P<version>[^]]+)] - (?P<date>\d{4}-\d{2}-\d{2})\s*$",
re.MULTILINE,
)

def parse_body(body):
if ';' not in body:
return body

parts = body.split(";")
points = []
for i, e in enumerate(parts):
e: str = e.strip()
if e == '':
def read_source_version(path: Path) -> str:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for node in tree.body:
if not isinstance(node, ast.Assign) or len(node.targets) != 1:
continue
points.append(f'{i + 1}. {e}')
target = node.targets[0]
if isinstance(target, ast.Name) and target.id == "__version__":
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
return node.value.value
raise ValueError(f"Static __version__ assignment not found in {path}")


def read_release_version(commit_message: str) -> str:
subject = commit_message.splitlines()[0].strip() if commit_message else ""
match = RELEASE_SUBJECT_PATTERN.fullmatch(subject)
if match is None:
raise ValueError(f"Release commit must match v{{version}}: summary, got: {subject}")
return match.group("version")


def extract_release_body(changelog: str, version: str) -> str:
matches = [match for match in VERSION_HEADING_PATTERN.finditer(changelog) if match.group("version") == version]
if not matches:
raise ValueError(f"Changelog section not found: ## [{version}] - YYYY-MM-DD")
if len(matches) > 1:
raise ValueError(f"Duplicate changelog sections found for version {version}")

match = matches[0]
next_heading = re.search(r"^## \[", changelog[match.end():], re.MULTILINE)
section_end = match.end() + next_heading.start() if next_heading else len(changelog)
body = changelog[match.end():section_end].strip()
if not body:
raise ValueError(f"Changelog section for version {version} is empty")
return body


def count_release_entries(body: str) -> int:
return sum(1 for line in body.splitlines() if line.lstrip().startswith("- "))


return '\n'.join(points)
def build_release_metadata(
commit_message: Optional[str] = None,
root_dir: Optional[Path] = None,
) -> Tuple[str, str]:
root_dir = root_dir or ROOT_DIR
source_version = read_source_version(root_dir / VERSION_FILE)
if commit_message is not None:
release_version = read_release_version(commit_message)
if release_version != source_version:
raise ValueError(
f"Version mismatch: release commit={release_version}, __init__.py={source_version}"
)

changelog = (root_dir / CHANGELOG_FILE).read_text(encoding="utf-8")
return f"v{source_version}", extract_release_body(changelog, source_version)

def get_tag_and_body():
msg = sys.argv[1]
print(f'msg: {msg}')
p = re.compile('(.*?): ?(.*)')
match = p.search(msg)
assert match is not None, f'commit message format is wrong: {msg}'
tag, body = match[1], match[2]
return body, tag

def add_output(key: str, value: str, output_path: Optional[str] = None) -> None:
output_path = output_path or os.environ.get("GITHUB_OUTPUT")
if output_path is None:
print(f"{key}={value}")
return
with Path(output_path).open("a", encoding="utf-8") as output_file:
output_file.write(f"{key}={value}\n")

def main():
body, tag = get_tag_and_body()

add_output('tag', tag)
def main(commit_message: Optional[str] = None) -> int:
try:
tag, body = build_release_metadata(commit_message)
(ROOT_DIR / RELEASE_BODY_FILE).write_text(f"{body}\n", encoding="utf-8")
add_output("tag", tag)
except (OSError, SyntaxError, ValueError) as exc:
print(f"Release metadata error: {exc}", file=sys.stderr)
return 1

with open('release_body.txt', 'w', encoding='utf-8') as f:
f.write(parse_body(body))
print(f"Release version: {tag.removeprefix('v')}")
print(f"Changelog entries: {count_release_entries(body)}")
print(f"Release body source: {CHANGELOG_FILE}")
return 0


main()
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1] if len(sys.argv) >= 2 else None))
1 change: 1 addition & 0 deletions requirements-dev.txt → .github/requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Pillow
psutil
pycryptodome
requests
rich
jm-view-server
zhconv
img2pdf
2 changes: 1 addition & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pip install -r .github/requirements-dev.txt
pip install -e .

- name: Run Performance Benchmark
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/download.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: Install Dependency
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pip install -r .github/requirements-dev.txt

- name: 安装jmcomic(pip)
if: ${{ github.ref != 'refs/heads/dev' }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/download_dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ jobs:
- name: Install Dependency
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pip install -r .github/requirements-dev.txt

- name: Install img2pdf
if: inputs.PDF_OPTION != '否'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/export_favorites.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
- name: Install Dependency
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pip install -r .github/requirements-dev.txt
sudo apt update
sudo apt install p7zip-full

Expand Down
29 changes: 21 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# publish when new release
name: Publish If Release
# manually release and publish from master
name: Manual Release & Publish

on:
workflow_dispatch:
release:
types: [ published ]

jobs:
release:
name: Publish `jmcomic` to PYPI
name: Manually release `jmcomic`
if: github.ref_name == 'master'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
steps:
- uses: actions/checkout@v4

Expand All @@ -18,12 +20,23 @@ jobs:
with:
python-version: "3.11"

- name: Build Release Metadata
id: tb
run: |
python .github/release.py

- name: Build
run: |
python -m pip install build
python -m build

- name: Publish PYPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Create Release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
password: ${{ secrets.PYPI_JMCOMIC }}
tag_name: ${{ steps.tb.outputs.tag }}
body_path: release_body.txt

- name: Release PYPI
uses: pypa/gh-action-pypi-publish@release/v1
Comment on lines +34 to 42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-remote https://github.com/softprops/action-gh-release refs/tags/v2
git ls-remote https://github.com/pypa/gh-action-pypi-publish refs/tags/release/v1

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 222


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Workflow file context:\n'
sed -n '1,90p' .github/workflows/release.yml 2>/dev/null || true

printf '\nAll action uses in workflows:\n'
rg -n 'uses:\s*[^#]+' .github/workflows || true

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 3084


🌐 Web query:

softprops action-gh-release v2 documentation permissions releases token write

💡 Result:

To use softprops/action-gh-release, you must configure your GitHub Actions workflow with the necessary permissions to allow the action to create releases [1][2]. Required Permissions The action requires the contents: write permission [1][3]. If you also use the discussion_category_name input, you must additionally provide the discussions: write permission [1][3]. You can set these permissions at the workflow or job level in your YAML configuration: permissions: contents: write If enabling discussions: permissions: contents: write discussions: write Important Token Considerations 1. Default Token: By default, the action uses the standard GITHUB_TOKEN [2][4]. Ensure that your repository settings (under Actions > General > Workflow Permissions) allow for Read and Write access [5]. 2. Personal Access Token (PAT): If you are running the workflow on the release event (e.g., on: { release: { types: [published] } }), you must provide a personal access token instead of the default GITHUB_TOKEN, because the default token will not trigger subsequent workflows [1][2]. 3. Overriding: You can explicitly pass a custom token using the token input if needed [2][6]. A non-empty explicit token will override the default GITHUB_TOKEN [4][6]. Note that v2 of this action is no longer maintained [2]. While the permission requirements remain the same, it is recommended to review the project's current status for migration paths [2].

Citations:


🌐 Web query:

pypa gh-action-pypi-publish release/v1 OIDC token packages publish README

💡 Result:

The pypa/gh-action-pypi-publish action (specifically the release/v1 branch) supports Trusted Publishing, which uses OpenID Connect (OIDC) to authenticate with PyPI without needing manual API tokens or passwords [1][2][3]. To use OIDC/Trusted Publishing with this action, you must follow these requirements: 1. Configure Trusted Publishing on PyPI: Your project on PyPI must already be configured to trust your GitHub repository as a publisher [1][3]. 2. Set Permissions: The GitHub Actions job running the publish step must have the id-token: write permission enabled [1][4]. 3. Omit Credentials: Do not provide an explicit username or password (or API token) to the action [1][2]. Example configuration for your GitHub Actions workflow: jobs: pypi-publish: name: Upload release to PyPI runs-on: ubuntu-latest environment: name: pypi permissions: id-token: write # Mandatory for Trusted Publishing steps: #... retrieve your distributions (e.g., download-artifact)... - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 Additionally, this action automatically generates and uploads PEP 740-compatible attestations using Sigstore, which uses the same OIDC identity [1][5]. If you need to use a self-hosted runner, Trusted Publishing is generally expected to work, provided it is configured correctly with GitHub [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Inspect Git references:\n'
git ls-remote https://github.com/softprops/action-gh-release refs/tags/v2 || true
git ls-remote https://github.com/softprops/action-gh-release refs/heads/v2 || true
git ls-remote https://github.com/softprops/action-gh-release refs/tags/v1 || true
git ls-remote https://github.com/pypa/gh-action-pypi-publish refs/tags/release/v1 || true
git ls-remote https://github.com/pypa/gh-action-pypi-publish refs/heads/release/v1 || true

printf '\nWorkflow permission blocks:\n'
rg -n -C 3 'permissions:|id-token|contents|Create Release|Release PYPI|GitHub Release|pypi-publish|action-gh-release' .github/workflows || true

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 3297


Pin the release actions to immutable commit SHAs.

softprops/action-gh-release@v2 runs with contents: write, and pypa/gh-action-pypi-publish@release/v1 runs with id-token: write for PyPI Trusted Publishing. Update both uses in .github/workflows/release.yml and .github/workflows/release_auto.yml to reviewed full commit SHAs instead of mutable tags.

🧰 Tools
🪛 zizmor (1.29.0)

[info] 29-29: action functionality is already included by the runner (superfluous-actions): use gh release in a script step

(superfluous-actions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 29 - 42, Update the release
workflow action references for softprops/action-gh-release and
pypa/gh-action-pypi-publish in both release workflows to reviewed, full
immutable commit SHAs, replacing the mutable v2 and release/v1 tags while
preserving their existing configuration.

Comment thread
hect0x7 marked this conversation as resolved.
14 changes: 6 additions & 8 deletions .github/workflows/release_auto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
name: Auto Release & Publish

on:
workflow_dispatch:
push:
branches:
- master
Expand All @@ -22,25 +21,24 @@ jobs:
with:
python-version: "3.11"

- name: Parse Tag & Body
- name: Build Release Metadata
id: tb
run: |
commit_message=$(git log --format=%B -n 1 ${{ github.sha }})
python .github/release.py "$commit_message"

- name: Build
run: |
python -m pip install build
python -m build

- name: Create Release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.tb.outputs.tag }}
body_path: release_body.txt
generate_release_notes: true

- name: Build
run: |
python -m pip install build
python -m build

- name: Release PYPI
uses: pypa/gh-action-pypi-publish@release/v1
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test_api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
paths:
- 'src/**/*.py'
- 'tests/**/*.py'
- '.github/requirements-dev.txt'
- '.github/workflows/test_api.yml'
- 'assets/option/option_test_api.yml'

Expand Down Expand Up @@ -36,7 +37,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
if [ -f .github/requirements-dev.txt ]; then pip install -r .github/requirements-dev.txt; fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Install local
run: |
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test_html.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
paths:
- 'src/**/*.py'
- 'tests/**/*.py'
- '.github/requirements-dev.txt'
- '.github/workflows/test_html.yml'
- 'assets/option/option_test_html.yml'

Expand Down Expand Up @@ -36,7 +37,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
if [ -f .github/requirements-dev.txt ]; then pip install -r .github/requirements-dev.txt; fi

- name: Install local
run: |
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# this repo
/assets/download/
/docs/superpowers/**


# Byte-compiled / optimized / DLL files
Expand Down Expand Up @@ -163,4 +164,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.agent
AGENTS.md
AGENTS.md
Loading
Loading