From 84d382dfd7f311d7d87b7e34d24b087e90602f89 Mon Sep 17 00:00:00 2001 From: toanalien Date: Tue, 7 Apr 2026 11:56:36 +0700 Subject: [PATCH 1/3] feat(release): build and publish EPUB artifacts per language Extend the release workflow to build separate EPUB artifacts for en, vi, and zh in parallel via a matrix job, then publish all built files under a single GitHub Release. Using fail-fast: false so a failure in one language does not block releasing the others. Also refactor build_epub.py to drive language-specific paths, filenames, and titles from a mapping, and add zh support (title/subtitle + choice). --- .github/workflows/release.yml | 53 ++++++++++++++++++++++++++++++++--- scripts/build_epub.py | 30 +++++++++++--------- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf92f435..518e0f7c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,9 +7,14 @@ on: jobs: build: + name: Build EPUB (${{ matrix.lang }}) runs-on: ubuntu-latest - permissions: - contents: write + strategy: + # Don't cancel other languages if one fails — we still want to release + # whichever languages built successfully. + fail-fast: false + matrix: + lang: [en, vi, zh] steps: - uses: actions/checkout@v4 @@ -25,11 +30,51 @@ jobs: - name: Install Mermaid CLI run: npm install -g @mermaid-js/mermaid-cli + - name: Install Python dependencies + run: | + uv venv + uv pip install -r scripts/requirements-dev.txt + - name: Build EPUB - run: uv run scripts/build_epub.py + run: | + echo '{"args":["--no-sandbox","--disable-setuid-sandbox"]}' > /tmp/puppeteer-ci.json + uv run scripts/build_epub.py \ + --lang ${{ matrix.lang }} \ + --puppeteer-config /tmp/puppeteer-ci.json + + - name: Upload EPUB artifact + uses: actions/upload-artifact@v4 + with: + name: epub-${{ matrix.lang }} + path: claude-howto-guide*.epub + if-no-files-found: error + retention-days: 7 + + release: + name: Publish GitHub Release + needs: build + # Release even if some language builds failed — we still publish the + # artifacts that did build. `needs.build.result != 'cancelled'` guards + # against manually cancelled runs. + if: ${{ always() && needs.build.result != 'cancelled' }} + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download all EPUB artifacts + uses: actions/download-artifact@v4 + with: + path: dist + pattern: epub-* + merge-multiple: true + + - name: List built artifacts + run: ls -lh dist/ - name: Create Release uses: softprops/action-gh-release@v2 with: - files: claude-howto-guide.epub + files: dist/*.epub generate_release_notes: true + fail_on_unmatched_files: true diff --git a/scripts/build_epub.py b/scripts/build_epub.py index e50b3c55..a0c7f35b 100755 --- a/scripts/build_epub.py +++ b/scripts/build_epub.py @@ -113,6 +113,8 @@ class EPUBConfig: vi_subtitle: str = "Làm chủ Claude Code trong một cuối tuần" en_title: str = "Claude Code How-To Guide" en_subtitle: str = "Master Claude Code in a Weekend" + zh_title: str = "Claude Code 使用指南" + zh_subtitle: str = "一个周末掌握 Claude Code" # Cover Settings cover_width: int = 600 @@ -1057,8 +1059,11 @@ def main() -> int: "--lang", type=str, default="en", - choices=["en", "vi"], - help="Language code: 'en' for English, 'vi' for Vietnamese (default: en)", + choices=["en", "vi", "zh"], + help=( + "Language code: 'en' for English, 'vi' for Vietnamese, " + "'zh' for Chinese (default: en)" + ), ) parser.add_argument( "--puppeteer-config", @@ -1073,17 +1078,16 @@ def main() -> int: repo_root = args.root if args.root else Path(__file__).parent.parent repo_root = repo_root.resolve() - # Set language-specific paths and metadata - if args.lang == "vi": - root = repo_root / "vi" - output = args.output or (repo_root / "claude-howto-guide-vi.epub") - title = EPUBConfig.vi_title - language = "vi" - else: - root = repo_root - output = args.output or (repo_root / "claude-howto-guide.epub") - title = EPUBConfig.en_title - language = "en" + # Set language-specific paths and metadata. + # Each entry: (source root, default output filename, title) + lang_map: dict[str, tuple[Path, str, str]] = { + "en": (repo_root, "claude-howto-guide.epub", EPUBConfig.en_title), + "vi": (repo_root / "vi", "claude-howto-guide-vi.epub", EPUBConfig.vi_title), + "zh": (repo_root / "zh", "claude-howto-guide-zh.epub", EPUBConfig.zh_title), + } + root, default_output_name, title = lang_map[args.lang] + output = args.output or (repo_root / default_output_name) + language = args.lang root = root.resolve() output = output.resolve() From 3885943071d0d012841ce4edcd7420bd2934286d Mon Sep 17 00:00:00 2001 From: Luong NGUYEN Date: Tue, 7 Apr 2026 09:03:22 +0200 Subject: [PATCH 2/3] =?UTF-8?q?fix(release):=20address=20review=20feedback?= =?UTF-8?q?=20=E2=80=94=20subtitle=20wiring,=20zh=20guard,=20empty-dist=20?= =?UTF-8?q?guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wire language-specific subtitle through EPUBConfig.subtitle field so create_cover_image renders the correct cover text per --lang instead of always using the hardcoded English defaults - Add continue-on-error for zh matrix leg (zh/ content not yet populated) and explicit permissions: contents: read on the build job - Add 'Check artifacts exist' guard step before Create Release so an all-languages-fail scenario produces a clear failure rather than confusing softprops errors; make 'List built artifacts' tolerant of an empty dist/ --- .github/workflows/release.yml | 20 +++++++++++++++++++- scripts/build_epub.py | 33 ++++++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 518e0f7c..c345cdf4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,12 +9,20 @@ jobs: build: name: Build EPUB (${{ matrix.lang }}) runs-on: ubuntu-latest + continue-on-error: ${{ matrix.continue-on-error == true }} + permissions: + contents: read strategy: # Don't cancel other languages if one fails — we still want to release # whichever languages built successfully. fail-fast: false matrix: lang: [en, vi, zh] + include: + # zh/ content is scaffolded but not yet populated; allow this leg + # to fail without blocking the release of en/vi EPUBs. + - lang: zh + continue-on-error: true steps: - uses: actions/checkout@v4 @@ -70,7 +78,17 @@ jobs: merge-multiple: true - name: List built artifacts - run: ls -lh dist/ + run: ls -lh dist/ 2>/dev/null || echo "dist/ is empty or does not exist" + + - name: Check artifacts exist + id: check_artifacts + run: | + count=$(ls dist/*.epub 2>/dev/null | wc -l) + echo "count=$count" >> $GITHUB_OUTPUT + if [ "$count" -eq 0 ]; then + echo "No EPUB artifacts were built — skipping release." + exit 1 + fi - name: Create Release uses: softprops/action-gh-release@v2 diff --git a/scripts/build_epub.py b/scripts/build_epub.py index a0c7f35b..6ec8bec9 100755 --- a/scripts/build_epub.py +++ b/scripts/build_epub.py @@ -105,6 +105,7 @@ class EPUBConfig: # EPUB Metadata identifier: str = "claude-howto-guide" title: str = "Claude Code How-To Guide" + subtitle: str = "Master Claude Code in a Weekend" language: str = "en" author: str = "Claude Code Community" @@ -908,7 +909,9 @@ def build_epub_async( # Add cover logger.info("Generating cover image...") - cover_data = create_cover_image(config, logger) + cover_data = create_cover_image( + config, logger, title=config.title, subtitle=config.subtitle + ) book.set_cover("cover.png", cover_data) # Add CSS @@ -1079,13 +1082,28 @@ def main() -> int: repo_root = repo_root.resolve() # Set language-specific paths and metadata. - # Each entry: (source root, default output filename, title) - lang_map: dict[str, tuple[Path, str, str]] = { - "en": (repo_root, "claude-howto-guide.epub", EPUBConfig.en_title), - "vi": (repo_root / "vi", "claude-howto-guide-vi.epub", EPUBConfig.vi_title), - "zh": (repo_root / "zh", "claude-howto-guide-zh.epub", EPUBConfig.zh_title), + # Each entry: (source root, default output filename, title, subtitle) + lang_map: dict[str, tuple[Path, str, str, str]] = { + "en": ( + repo_root, + "claude-howto-guide.epub", + EPUBConfig.en_title, + EPUBConfig.en_subtitle, + ), + "vi": ( + repo_root / "vi", + "claude-howto-guide-vi.epub", + EPUBConfig.vi_title, + EPUBConfig.vi_subtitle, + ), + "zh": ( + repo_root / "zh", + "claude-howto-guide-zh.epub", + EPUBConfig.zh_title, + EPUBConfig.zh_subtitle, + ), } - root, default_output_name, title = lang_map[args.lang] + root, default_output_name, title, subtitle = lang_map[args.lang] output = args.output or (repo_root / default_output_name) language = args.lang @@ -1098,6 +1116,7 @@ def main() -> int: output_path=output, language=language, title=title, + subtitle=subtitle, mmdc_path=args.mmdc_path, puppeteer_config=args.puppeteer_config, ) From 40a697c5167759e5ba3a5d5336d33b957a9d1fdd Mon Sep 17 00:00:00 2001 From: Luong NGUYEN Date: Tue, 7 Apr 2026 09:08:54 +0200 Subject: [PATCH 3/3] refactor(build_epub): replace fragile dataclass class-attr access with module constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Language metadata (subdir, filename, title, subtitle) was stored as EPUBConfig instance fields and then accessed as class-level attributes in lang_map — a fragile pattern that silently breaks if any field loses its default. Move this data to a module-level _LANG_METADATA dict and remove the now-redundant vi_/en_/zh_ title/subtitle fields from EPUBConfig. --- scripts/build_epub.py | 57 ++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/scripts/build_epub.py b/scripts/build_epub.py index 6ec8bec9..fd93e5ed 100755 --- a/scripts/build_epub.py +++ b/scripts/build_epub.py @@ -92,6 +92,29 @@ class CoverGenerationError(EPUBBuildError): # Configuration and State # ============================================================================= +# Language metadata: maps lang code → (subdir, output filename, title, subtitle). +# "" subdir means use the repo root (English content lives at the top level). +_LANG_METADATA: dict[str, tuple[str, str, str, str]] = { + "en": ( + "", + "claude-howto-guide.epub", + "Claude Code How-To Guide", + "Master Claude Code in a Weekend", + ), + "vi": ( + "vi", + "claude-howto-guide-vi.epub", + "Hướng Dẫn Claude Code", + "Làm chủ Claude Code trong một cuối tuần", + ), + "zh": ( + "zh", + "claude-howto-guide-zh.epub", + "Claude Code 使用指南", + "一个周末掌握 Claude Code", + ), +} + @dataclass class EPUBConfig: @@ -109,14 +132,6 @@ class EPUBConfig: language: str = "en" author: str = "Claude Code Community" - # Language-specific metadata - vi_title: str = "Hướng Dẫn Claude Code" - vi_subtitle: str = "Làm chủ Claude Code trong một cuối tuần" - en_title: str = "Claude Code How-To Guide" - en_subtitle: str = "Master Claude Code in a Weekend" - zh_title: str = "Claude Code 使用指南" - zh_subtitle: str = "一个周末掌握 Claude Code" - # Cover Settings cover_width: int = 600 cover_height: int = 900 @@ -1081,29 +1096,9 @@ def main() -> int: repo_root = args.root if args.root else Path(__file__).parent.parent repo_root = repo_root.resolve() - # Set language-specific paths and metadata. - # Each entry: (source root, default output filename, title, subtitle) - lang_map: dict[str, tuple[Path, str, str, str]] = { - "en": ( - repo_root, - "claude-howto-guide.epub", - EPUBConfig.en_title, - EPUBConfig.en_subtitle, - ), - "vi": ( - repo_root / "vi", - "claude-howto-guide-vi.epub", - EPUBConfig.vi_title, - EPUBConfig.vi_subtitle, - ), - "zh": ( - repo_root / "zh", - "claude-howto-guide-zh.epub", - EPUBConfig.zh_title, - EPUBConfig.zh_subtitle, - ), - } - root, default_output_name, title, subtitle = lang_map[args.lang] + # Resolve language-specific paths and metadata from the module-level constant. + subdir, default_output_name, title, subtitle = _LANG_METADATA[args.lang] + root = repo_root / subdir if subdir else repo_root output = args.output or (repo_root / default_output_name) language = args.lang