diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf92f435..c345cdf4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,9 +7,22 @@ on: jobs: build: + name: Build EPUB (${{ matrix.lang }}) runs-on: ubuntu-latest + continue-on-error: ${{ matrix.continue-on-error == true }} permissions: - contents: write + 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 @@ -25,11 +38,61 @@ 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/ 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 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..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: @@ -105,15 +128,10 @@ 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" - # 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" - # Cover Settings cover_width: int = 600 cover_height: int = 900 @@ -906,7 +924,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 @@ -1057,8 +1077,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 +1096,11 @@ 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" + # 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 root = root.resolve() output = output.resolve() @@ -1094,6 +1111,7 @@ def main() -> int: output_path=output, language=language, title=title, + subtitle=subtitle, mmdc_path=args.mmdc_path, puppeteer_config=args.puppeteer_config, )