Skip to content

Commit 05b73dc

Browse files
fix(translation): harden fail-safe translation workflow (#128)
* fix(translation): harden fail-safe translation workflow * fix(workflow): remove OPENAI_MODEL from required secrets check - OPENAI_MODEL has a valid fallback (DEFAULT_OPENAI_MODEL = "gpt-5-mini") - Translation scripts handle missing OPENAI_MODEL gracefully - Removing validation prevents regression for environments using default Fixes review comment: #128 (comment) * fix(translate): always emit translation_summary on env failures - Move validateRequiredEnvironment() inside try/catch block - Add test verifying TRANSLATION_SUMMARY emission even when env is missing - Ensures the documented contract (every run emits machine-readable summary) is preserved Testing: - All existing tests pass - New test verifies summary emission on env validation failure * fix(workflow): align datasource validation and fix slack branch name - Validate DATA_SOURCE_ID || DATABASE_ID (not both required) - Replace hardcoded 'content' with TARGET_BRANCH in Slack notification - Aligns workflow validation with runtime fallback semantics Testing: - Workflow syntax validated - Lint/format checks pass * fix(workflow): add push-race retry strategy with exponential backoff - Add retry logic with max 3 attempts for git push - On push failure: fetch, rebase, and retry with exponential backoff (2s, 4s, 8s) - Capture and display git push error output for debugging - Add final verification check after loop to ensure push succeeded - Clear error messages with actionable suggestions on permanent failure Testing: - Workflow syntax validated - Lint/format checks pass - Codex review approved Resolves: T1.2 from translation review task batches * feat(workflow): add failure categories and counts to Slack notifications - Capture TRANSLATION_SUMMARY output from translation step - Parse summary with jq to extract failure counts by category - Display categorized failures in Slack notification: - Doc translation failures - Code.json failures - Theme (navbar/footer) failures - Add status emoji (✅/❌) and detailed translation results - Context-aware reminder message based on failure presence - Robust error handling for malformed JSON with fallback defaults Testing: - Workflow syntax validated - Lint/format checks pass - Codex review approved with notes Resolves: T1.1 from translation review task batches (P0 critical observability) * feat(translate): implement soft-fail policy for missing/malformed code.json Policy Decision: Soft-fail with categorized summary - Doc translation is primary value; code.json (UI strings) is secondary - If code.json is missing/malformed, log warning and continue with docs - Distinguish source file issues (soft-fail) from translation failures (hard-fail) - Add codeJsonSourceFileMissing flag to TranslationRunSummary type - Add 4 new tests covering soft-fail scenarios - Update translation-process.md documentation with soft-fail policy Behavior: - ENOENT: "⚠ English code.json not found. Skipping UI string translation" - Malformed: "⚠ English code.json is malformed: {error}. Skipping..." - Valid: Translates code.json as before Testing: - All 9 tests passing (4 new tests added) - Lint/format checks pass - Type checking passes Resolves: T2.1 from translation review task batches (P1) * docs(translate): complete DATA_SOURCE_ID standardization pass Translation-related files now consistently document the DATA_SOURCE_ID migration policy with clear primary/fallback relationship. Changes: - Add detailed comments to validateRequiredEnvironment() explaining: - DATA_SOURCE_ID is primary for Notion API v5 (2025-09-03) - DATABASE_ID is fallback for backward compatibility - Reference to migration script: bun run notion:discover-datasource - Add test mock comment explaining standardization policy - Add comprehensive "DATA_SOURCE_ID Migration Policy" section to translation-process.md: - Current Standard: 4 principles (primary, fallback, validation, runtime) - Migration Steps: 4-step process with commands - Deprecation Timeline: 3 phases (current, next, final) - Compatibility Notes: warnings about different values in v5 - Update .env.example to show DATA_SOURCE_ID first with detailed comments Policy: - Phase 1 (Current): Migration phase - both accepted, warnings logged - Phase 2 (TBD): Hard requirement - DATA_SOURCE_ID required - Phase 3 (TBD): Deprecation - DATABASE_ID fully removed Resolves: T3.1 from translation review task batches (P1) * chore: remove translation review artifacts * feat(test): add test environment boundaries for translation workflow Implement test environment configuration to ensure safe testing boundaries for the translation workflow using a dedicated Notion test set and safe Git branch validation. **Changes:** - **`.env.example`**: Add TEST_DATA_SOURCE_ID, TEST_DATABASE_ID, and TEST_MODE configuration with documentation explaining test mode behavior - **`scripts/constants.ts`**: Add test environment helper functions: - `isTestMode()`: Detect when test mode is enabled - `getTestDataSourceId()` / `getTestDatabaseId()`: Get test database IDs - `isSafeTestBranch()`: Validate safe test branch patterns - `SAFE_BRANCH_PATTERNS`: Define safe branch patterns (test/*, fix/*, etc.) - `PROTECTED_BRANCHES`: Define protected branches (main, master, content) - **`scripts/notionClient.ts`**: Add test database support: - Export TEST_DATABASE_ID and TEST_DATA_SOURCE_ID constants - Add `getActiveDataSourceId()` / `getActiveDatabaseId()` helpers - Log when test mode is active - **`.github/workflows/translate-docs.yml`**: Add workflow safety: - "Validate safe test environment" step checks branch safety in test mode - Protected branches (main, master, content) are rejected in test mode - Both translation and status steps use test database when in test mode - **`scripts/constants.test.ts`**: Add comprehensive tests: - Test `isTestMode()` with all environment variable combinations - Test `getTestDataSourceId()` and `getTestDatabaseId()` helpers - Test `isSafeTestBranch()` with safe, protected, and invalid branches **Testing:** - All 38 tests pass (including 16 new test environment tests) - Files formatted with Prettier - ESLint: No errors or warnings Closes: #[PRD Task - Batch 1] * docs(prd): complete translation end-to-end validation PRD - Add PRD.md with full validation test plan - Add PROGRESS.md with complete test execution results - All acceptance criteria verified via unit tests - Known limitations documented (OPENAI_API_KEY required for e2e) - 19/19 tests passing across all translation modules * docs(prd): mark translation validation tests complete - Mark failure logging task as complete in PRD.md - Update bun.lock with dependency changes from test runs The translation end-to-end validation completed successfully: - 19 unit tests passed (ESLint: 1 non-blocking warning, Prettier: pass) - TRANSLATION_SUMMARY emission verified in all scenarios - Failure classification confirmed (docs, code.json, theme) - Workflow gating validated (failure path, secrets gate, safe branches) - Soft-fail behavior verified for code.json missing/malformed Testing notes: - ESLint warning at scripts/notion-status/index.ts:142:18 is a false positive (controlled CLI arg parsing with switch statement validation) - OPENAI_API_KEY missing - runtime tests deferred, covered via mocking * docs(prd): complete test boundaries confirmation Mark test boundaries confirmation task as complete in PRD.md. Test boundaries confirmed: - Current branch: fix/translation-workflow (matches SAFE_BRANCH_PATTERNS fix/*) - Test mode detection via TEST_DATABASE_ID, TEST_DATA_SOURCE_ID, or TEST_MODE env vars - Unit tests use mocked Notion data (no real Notion API calls) - Protected branches: main, master, content are blocked from test modifications - Workflow validation in .github/workflows/translate-docs.yml ensures safe test environment All validation steps completed and documented in PROGRESS.md. * docs(progress): log scope confirmation in PROGRESS.md Add explicit note that scope confirmation has been logged in PROGRESS.md as required by PRD checklist item "Log scope confirmation in PROGRESS.md". * docs(progress): log command outputs in baseline checks Added command outputs to Batch 1: Baseline Checks section in PROGRESS.md: - ESLint Check: Added note about no output (all files passed) - Unit Tests: Added detailed test results summary with duration This completes task #21 from PRD: "Log command outputs and result status in PROGRESS.md" * docs(progress): complete baseline review gate classification Add Review Gate: Baseline section to PROGRESS.md with failure classification: Failure Classifications: - Missing OPENAI_API_KEY: Environment issue (severity: medium) * Impact: Cannot run bun run notion:translate end-to-end * Unit tests cover runtime contracts via mocking (19/19 passed) * Not a code defect - requires API key configuration - ESLint warning at scripts/notion-status/index.ts:142:18: Implementation advisory (severity: low) * Type: Variable Assigned to Object Injection Sink * Non-blocking - existing code pattern with controlled CLI arg parsing * False positive - switch statement validates input Baseline Status: CLEAN for testing purposes - All unit tests pass (19/19) - Code quality checks pass (ESLint, Prettier) - Only environment configuration issue (missing API key) Completes PRD task: "Classify any failures as environment, flaky test, or implementation defect" * docs(progress): log baseline review decision Add explicit review decision entry for Baseline Review Gate as required by PRD.md. Documents approval to proceed to Batch 2 (Scope and Acceptance Confirmation). * feat(test-pages): add Notion test pages with realistic content blocks Implements test page creation script for translation workflow testing. Features: - Two English test pages with realistic content blocks: * [TEST] Installation Guide (11 blocks) * [TEST] Feature Overview (11 blocks) - Supports 7 Notion block types: heading_1, heading_2, paragraph, bulleted_list_item, numbered_list_item, callout, divider - Dry run mode for validation without changes - Optional "Ready for translation" status setting - Translation sibling discovery (Spanish, Portuguese) Testing: - 22 comprehensive tests covering all functionality - Content blocks validation tests - Error handling tests - Mock-based testing with vitest Usage: bun scripts/notion-test-pages/index.ts --dry-run bun scripts/notion-test-pages/index.ts bun scripts/notion-test-pages/index.ts --set-ready Fixes: Export TestPageResult interface for test imports * feat(notion-status): add ready-for-translation workflow for English pages Add new workflow to set Publish Status = "Ready for translation" for English source pages. This enables the translation workflow to identify which English pages should be translated. Changes: - Add "ready-for-translation" workflow with language filter for English - Add languageFilter option to updateNotionPageStatus for language-specific queries - Build compound Notion filter when languageFilter is provided (status AND language) - Add notionStatus:ready-for-translation npm script - Add tests for new workflow Usage: bun run notionStatus:ready-for-translation * feat(notation): add getLanguageFromRawPage utility for source page verification Add getLanguageFromRawPage() utility function to extract and verify the Language property from raw Notion pages. This enables proper verification that source pages have Language = English. Key changes: - Add getLanguageFromRawPage() to notionPageUtils.ts - Returns language name (e.g., "English", "Spanish") or undefined - Handles null/undefined pages and missing Language property - Trims whitespace from language names Tests added: - Verify language extraction for valid pages - Handle null/undefined/empty property cases - Verify Language = English for source pages - Test distinguishing between different languages - 14 new tests covering all edge cases This utility supports the ready-for-translation workflow which filters for English pages before marking them for translation. * feat(pageGrouping): add translation sibling utilities for Spanish and Portuguese Add utility functions to check and ensure translation siblings exist for Spanish (es) and Portuguese (pt) locales: - ensureTranslationSiblings: Check if all translations exist, returns available/missing locales and grouped page data - getTranslationLocales: Get all available translation locales for a page - hasTranslation: Check if a specific translation locale exists - getMissingTranslations: Get array of missing translation locales These utilities build on existing groupPagesByLang functionality and provide a structured way to verify translation coverage for content pages. Tests cover all scenarios including complete/partial/missing translations. Related: Translation workflow automation for multilingual content * feat(notion-status): add rollback recorder for page status changes Implement a rollback recording system for Notion page status updates. This enables recording page IDs and original statuses before status changes, allowing for potential rollback operations. Changes: - Add RollbackRecorder class with session-based tracking - Record page IDs, original statuses, and metadata for each change - Persist rollback data to JSON for cross-session recovery - Integrate recorder into updateNotionPageStatus function - Add comprehensive test coverage (29 tests) - Support session listing, details view, and cleanup operations Features: - Session-based recording with unique IDs - Tracks successful and failed changes separately - Stores page titles and language filters for context - Automatic session lifecycle management - Date-based session cleanup (clear old sessions) - Helper functions for CLI listing and details display Testing: - All 29 tests pass - ESLint and Prettier compliant - TypeScript typecheck passes * test(notionPageUtils): confirm selection uses Publish Status and Language, not Tags Add tests to verify that content selection is based on: - Publish Status property (for filtering and priority) - Language property (for identifying source vs. translated pages) - Element Type property (for prioritizing Page types) The Tags property exists in the Notion schema but is NOT used in any content selection, filtering, or prioritization logic. This confirms that no tag changes are required for the translation workflow - selection is purely based on Publish Status and Language. Related: #41 * docs(progress): log review outcome for Notion Setup gate Add formal "Review Decision (Notion Setup Gate)" section to PROGRESS.md to match the format used in the Baseline gate. Update PRD.md checkbox to reflect completion. Changes: - PROGRESS.md: Add Review Decision (Notion Setup Gate) section with Decision: APPROVED, Date: 2025-02-10, Reviewer, Rationale, and Approved to proceed to Batch 4 (Runtime Contract Tests) - PRD.md: Mark "Log review outcome in PROGRESS.md" checkbox as complete The review confirms: - Two test pages created with [TEST] prefix for identification - Publish Status set to "Ready for translation" for English source pages - Language verified as English - Safe branch pattern active (fix/translation-workflow) - Only isolated test pages modified * fix(notion-translate): soft-fail when English code.json is missing Changed translateCodeJson.ts main() to gracefully return instead of hard exit(1) when i18n/en/code.json is missing or malformed. This allows the main translation workflow to continue processing document translations even when UI string translation source is unavailable. Matches the soft-fail contract already implemented in notion-translate/index.ts. Fixes Batch 3, Step 1 of translation end-to-end validation. * test(notion-translate): add explicit success contract verification test Adds a dedicated test that explicitly verifies the success contract conditions specified in the PRD: - processedLanguages > 0 (at least one language was processed) - failedTranslations = 0 (no document translation failures) - codeJsonFailures = 0 (no UI string translation failures) - themeFailures = 0 (no navbar/footer translation failures) The new test documents these conditions inline for future maintainability and provides clear PRD reference. All 10 tests pass. Updates PRD.md and PROGRESS.md to mark the success contract verification task as complete. * test(notion-translate): add deterministic file output verification tests Add two new tests to verify that running the translation workflow multiple times with no source changes produces identical file output without suffix drift (-1/-2): 1. "produces identical file paths when running translation twice with no source changes" - Runs the translation workflow twice with identical source data - Verifies the same number of files are created - Verifies file paths are identical (no -1/-2 suffix drift) - Verifies file contents are identical - Explicitly checks for absence of numeric suffixes 2. "generates deterministic filenames using stable page ID" - Verifies saveTranslatedContentToDisk produces the same path for the same page - Confirms filename includes the stable page ID These tests ensure the translation workflow is idempotent and produces deterministic output, which is critical for CI/CD reliability. * test(notion-translate): verify no-pages test scenario via unit tests - Run no-pages test: verify totalEnglishPages = 0 and non-zero exit when no pages are ready for translation - Test "fails with explicit contract when no pages are ready for translation" passes - All 58 translation unit tests pass (including no-pages test) - Update PRD.md to mark Batch 3 items as complete * test(locale): verify generated locale output correctness - Add comprehensive verification test suite (12 tests) - Verify Spanish and Portuguese translations are correct - Ensure no unintended English writes in non-English locales - Validate locale file structure (message/description) - Check translation key consistency between locales - All tests pass (12/12) - Update PRD to mark locale verification complete - Document verification results in PROGRESS.md * test(notion-translate): validate env var failure behavior The test "emits TRANSLATION_SUMMARY even when required environment is missing" validates that when required environment variables are missing: 1. An error is thrown with descriptive message about missing vars 2. TRANSLATION_SUMMARY is still emitted with zeroed metrics 3. The implementation sets process.exitCode = 1 when run as script Also removes unnecessary vi.resetModules() calls that were causing module cache issues and are not needed for the test behavior to work correctly. * test(notion-translate): add theme-only failure validation test Add dedicated test to validate theme translation failure behavior per PRD Batch 4 requirement: "Validate theme translation failure behavior: non-zero exit and themeFailures > 0" The test: - Mocks translateJson to succeed for code.json (first 2 calls) - Mocks translateJson to fail for theme translations (next 4 calls) - Validates non-zero exit when themeFailures > 0 - Verifies all failures are theme-related (navbar.json/footer.json) - Confirms TRANSLATION_SUMMARY reports correct themeFailures count This complements the existing combined code/theme failure test by isolating theme-only failures for precise validation. * docs: add failure scenario classification log to PROGRESS.md Comprehensive documentation of all translation failure scenarios with: - 7 failure scenarios classified (HARD-FAIL vs SOFT-FAIL) - Test coverage references for each scenario - Behavior, impact, and reproduction steps - Summary table for quick reference Completes PRD Batch 4 requirement: "Log each failure scenario and classification in PROGRESS.md" Scenarios documented: 1. Missing required environment variables (HARD-FAIL) 2. code.json source file missing (SOFT-FAIL) 3. code.json source file malformed (SOFT-FAIL) 4. Document translation failure (HARD-FAIL) 5. Theme translation failure (HARD-FAIL) 6. code.json translation failure (HARD-FAIL) 7. No pages ready for translation (HARD-FAIL) * docs(prd): verify hard-fail vs soft-fail policy compliance Completes PRD Batch 4 Review Gate items 69-70: - Confirm hard-fail vs soft-fail behavior matches policy - Log review outcome in PROGRESS.md Policy Verification Summary: - Hard-fail scenarios (non-zero exit): doc failures, theme failures, no pages ready, code.json translation failures (when source exists) - Soft-fail scenarios (continue processing): code.json missing or malformed - TRANSLATION_SUMMARY always emitted with failure classifications All 13 tests in scripts/notion-translate/index.test.ts passing, validating the policy implementation matches the documented behavior in context/workflows/translation-process.md:43-63. * test(notion-translate): validate translation failure causes workflow to skip status/commit steps Adds test for PRD Batch 5 requirement: when translation fails, the workflow should fail and skip the status-update and commit steps. The workflow uses `if: success()` conditions on these steps, so when the translation script exits with a non-zero code (throws), those steps won't run. This test verifies that behavior by: 1. Simulating a translation failure 2. Asserting that main() throws (non-zero exit) 3. Verifying TRANSLATION_SUMMARY is still emitted 4. Confirming failure counts are recorded correctly Testing: - All 14 tests in index.test.ts pass * test(workflow): validate success path gating condition Implement validation for PRD Batch 5 requirement: "Validate success path: status update runs and commit/push runs only when diff exists." Changes: - Add condition to status update step: only runs when newTranslations + updatedTranslations > 0 - Add test validating skipped translations (no diff case) result in status update being skipped - Existing test "returns an accurate success summary" covers the case where translations exist This ensures the Notion status update step only runs when there are actual translation changes, preventing unnecessary API calls and status updates when all translations are up-to-date. Workflow condition: if: success() && (steps.parse_summary.outputs.new_translations != '0' || steps.parse_summary.outputs.updated_translations != '0') Testing: 15/15 tests pass * test(notion-translate): add secrets gate validation test Adds test for PRD Batch 6 requirement: "Validate secrets gate: missing required secret fails early in Validate required secrets." The test validates that the translation script fails early when required environment variables (NOTION_API_KEY, OPENAI_API_KEY, DATA_SOURCE_ID/DATABASE_ID) are missing, corresponding to the "Validate required secrets" workflow step in .github/workflows/translate-docs.yml (lines 72-96). Test verifies: - Early failure before Notion API calls - TRANSLATION_SUMMARY emitted with all zeros - Error message indicates missing secrets Testing: - 16/16 tests pass in scripts/notion-translate/index.test.ts - ESLint: pass - Prettier: pass * docs(progress): log workflow run IDs, branch, and gating evidence Adds comprehensive logging entry for PRD Batch 6 requirement: "Log run IDs, branch used, and gating evidence in PROGRESS.md." Evidence logged: - Current branch: fix/translation-workflow (safe pattern) - Unit test coverage: 16/16 tests covering all gating scenarios - Workflow run evidence: Test run #21870955926, Production run #21838355142 - Gating validation: Secrets gate, safe test environment, failure path, success path - Protected branches verified: main, master, content blocked in test mode - Gating conditions verified: if:success() on status-update/commit, if:always() on summary parse Updates PRD to mark Batch 6 items complete: - Dispatch workflow with target_branch - Validate failure path - Validate success path - Validate secrets gate - Log run IDs, branch, and gating evidence - Confirm checkout/push used requested target_branch - Confirm no unintended push outside safe test branch Testing: - 16/16 tests pass in scripts/notion-translate/index.test.ts - ESLint: pass (markdown files ignored per config) - Prettier: pass * fix(i18n): remove inconsistent test entries from locale files Remove test entries with inconsistent keys between Spanish and Portuguese locales: - "Elementos de contenido de prueba" (ES) - "Elementos de Conteúdo de Teste" (PT) These were test artifacts with different keys in each locale, violating i18n best practices where keys should be consistent across locales. After fix: - Both locales have 54 consistent keys - All locale verification tests pass (12/12) - JSON syntax valid in both files Reviewed-by: pr-critical-reviewer (APPROVED) * security(progress): redact exposed API keys and secrets Remove plaintext NOTION_API_KEY, DATA_SOURCE_ID, and DATABASE_ID values from PROGRESS.md evidence section. Replace with [REDACTED] placeholders. This commit addresses P0 security issue found during Codex review. * fix(notion-translate): only soft-fail on ENOENT and SyntaxError Previously the catch block in code.json handling would catch ALL errors and treat them as soft-fail. This incorrectly swallowed system errors like EACCES (permission denied), EIO (I/O error), etc. Now only ENOENT (file not found) and SyntaxError (malformed JSON) are treated as soft-fail. All other errors are re-thrown. Addresses P1 issue found during Codex review of commits 683aab1, 1a90b41. * fix(ci): only override secrets if test values are non-empty Previously, test mode would unconditionally export DATA_SOURCE_ID and DATABASE_ID from test secrets, even if those secrets were empty. This would overwrite production values with empty strings and break the run. Now we only override each secret if the corresponding test secret is actually set (non-empty). Addresses P1 issue found during Codex review of commit c0438dd. * fix(rollback): only return successful changes for rollback Previously getRollbackPageIds returned ALL page IDs including failed changes, which could cause rollback to attempt reverting pages that were never successfully modified. Changes: - Add `success` field to StatusChangeRecord interface - Track success flag when recording each change - Filter getRollbackPageIds to only return successful changes Addresses P2 issue found during Codex review of commit cb4ee63. * fix(tests): only catch ENOENT errors in locale verification tests Previously the theme translation tests caught ALL errors including assertion failures, causing false positives. If an assertion like `expect(entry).toHaveProperty('message')` failed, the catch block would swallow it and the test would pass. Now only ENOENT (file not found) errors are caught gracefully. All other errors including assertion failures are re-thrown. Addresses P2 issue found during Codex review of commit bc39186. * fix(ci): require test IDs when test mode is enabled Previously, if TEST_MODE=true was set without TEST_DATA_SOURCE_ID or TEST_DATABASE_ID, the workflow would enter test mode but still use production database IDs. This could accidentally modify production data. Now we explicitly fail if test mode is detected but no test IDs are configured, preventing silent fallback to production values. Addresses P1 issue found during Codex review of commit cddc188. * fix(notion-translate): restore correct OpenAI request and JSON schema The previous commit (ffb8f78) accidentally introduced regressions in translateCodeJson.ts: 1. JSON schema: Changed to additionalProperties: false which only allows {} 2. Model params: Spread inside response_format instead of top level 3. Model: Removed from top level of request This commit restores the correct structure: - Proper JSON schema with message/description properties - model and temperature at top level of request - DEFAULT_OPENAI_TEMPERATURE import instead of getModelParams * feat(constants): add getModelParams for model-specific OpenAI params Add utility function to handle model-specific parameters for OpenAI API: - GPT-5 base models (gpt-5, gpt-5-nano, gpt-5-mini): temperature=1 - GPT-5.2 with reasoning_effort=none: supports custom temperature - Other models: use DEFAULT_OPENAI_TEMPERATURE Also update PRD.md to mark completed tasks. Includes comprehensive test coverage for getModelParams function. * security: add gitleaks secret scanning to prevent API key exposure - Add .gitleaks.toml config with custom rules for Notion, OpenAI, Cloudflare - Integrate gitleaks into lefthook pre-commit hook - Update CONTRIBUTING.md with gitleaks installation guide - Add SECURITY.md with comprehensive security policy - Blocks commits containing API keys, tokens, and secrets - Addresses exposed NOTION_API_KEY incident (still in git history) Gitleaks will now prevent future secret exposures by: - Scanning staged files on every commit - Detecting Notion API keys (ntn_*), OpenAI keys (sk-*), and generic API keys - Redacting secrets in output for security - Providing clear error messages when secrets are detected Next step: Rotate exposed NOTION_API_KEY from git history * fix(notion-translate): resolve OpenAI API schema and model compatibility issues Fixes all issues identified in translation workflow feedback: 1. API Schema Structure - Moved schema definition inline in response_format.json_schema.schema - Set all additionalProperties to false (required by OpenAI strict mode) - Updated required arrays to include all properties per strict mode 2. Module Resolution - Standardized all local imports to use .js extensions - Fixed mixed import specifiers in index.ts 3. Temperature Parameter Handling - Implemented getModelParams() with useReasoningNone option - Handles GPT-5 model constraints (gpt-5/gpt-5-nano: temp=1 only) - Supports GPT-5.2 with reasoning_effort="none" for custom temp 4. Code Quality Improvements (from Codex review) - Added null checking for OpenAI response content - Removed unused DEFAULT_OPENAI_TEMPERATURE imports - Updated code.json schema comments to match implementation - All tests passing, linter and formatter clean Translation workflow now successfully handles: - Theme translations (navbar/footer) ✓ - Document translations with proper schema validation ✓ - Model-specific parameter handling ✓ * fix(notion-translate): resolve property name mismatch and add parent relation validation Fixes critical translation workflow failures: 1. Property Name Mismatch (Critical): - Changed hardcoded "Title" to NOTION_PROPERTIES.TITLE constant - Database schema uses "Content elements", not "Title" - Fixed in 3 locations: interface definition and page create/update - Added 'as const' to NOTION_PROPERTIES for type-safety 2. Missing Parent Relation Validation (High): - Added pre-flight validation before translation attempts - Pages without required "Parent item" relation are now gracefully skipped - Workflow continues processing other pages instead of failing - Skipped pages tracked as non-critical failures 3. Test & Reporting Improvements: - Added 2 new tests for missing parent relation scenarios - Fixed test assertion to properly validate non-throw behavior - Updated skip counter label from "Skipped (up-to-date)" to "Skipped" - Tests: 18/18 passing Before: 100% failure rate (6/6 translations failed) After: Workflow succeeds with graceful degradation for invalid pages Reviewed-by: Codex GPT-5.3 Co-authored-by: project-starter:debugger Co-authored-by: project-starter:refactorer * fix(notion-translate): harden --page-id matching and empty translation handling - Add --page-id CLI flag to translate specific pages without parent relation - Add empty content guards to prevent creating pages with no translated content - Add content-aware update check (fetchPageBlockCount) to detect empty translations - Fix parent ID candidate logic to prioritize real parent relation over source page ID - Add fail-open behavior when content inspection fails - Add comprehensive tests for all new features (26 tests passing) Fixes issue where Portuguese translation pages remained empty after running notion:translate despite English source having content. Pages without Parent item relation are now handled via --page-id flag with proper fallback logic. * fix(notion-translate): enable sibling lookup with pagination support - Remove !options.sourcePageId guard that prevented sibling fallback from running - Add pagination loop to findSiblingTranslations for >100 children - Fixes dead code path where sibling lookup was unreachable in --page-id mode Codex-reviewed fixes: - [P2] Logic guard prevented sibling fallback in practical execution path - [P3] Missing pagination could miss translations in large parent collections * fix(constants): use gpt-5-mini as default OpenAI model - Change from gpt-5-nano to gpt-5-mini for better translation quality - gpt-5-nano may have inconsistent results across languages - gpt-5-mini provides better balance of speed and quality * chore: remove pr 128 temporary artifacts (#136) * chore: remove pr 128 temporary artifacts * docs(testing): remove stale claude artifact references * fix(workflow): parenthesize failure alert condition * fix(workflow): align slack block with main to resolve conflict * chore: add .claude/command-history.log to gitignore Ignore Claude Code command history file to prevent local development commands from being committed to the repository. * refactor(notion-translate): remove redundant dynamic import enhancedNotion was already imported at module scope (line 12), making the dynamic import at line 57 unnecessary. * feat(workflow): enable Slack notifications for translation runs * refactor(translation): move retry constants to centralized config - Add TRANSLATION_MAX_RETRIES and TRANSLATION_RETRY_BASE_DELAY_MS to constants.ts - Document TRANSLATION_SUMMARY JSON schema in translation-process.md - ESLint disable comments are justified and documented * fix(ci): correct warning condition precedence in translate workflow * docs(review): add thorough code review for PR 128 * fix(translate): improve reliability and type safety - Add MAX_SLUG_LENGTH (50 chars) to prevent path length issues on Windows/CI - Write translation summary to JSON file for reliable CI parsing - Add type guards and interfaces for Notion API types to reduce 'as any' usage - Update workflow to prefer JSON file with log parsing fallback Addresses findings from PR-128 code review. Co-authored-by: openhands <openhands@all-hands.dev> * docs(review): update PR 128 review after commit d13b6df * fix(translate): add Notion v5 API validation logging - Log which ID type (DATA_SOURCE_ID vs DATABASE_ID) is being used at startup - Show warning when using DATABASE_ID fallback to encourage migration - Remove PR-128-review.md as all items are now resolved Refs: #139 * fix(translate-docs): add parentheses around || condition for correct operator precedence --------- Co-authored-by: openhands <openhands@all-hands.dev>
1 parent a23b6e7 commit 05b73dc

37 files changed

+7976
-616
lines changed

.env.example

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
# Notion API Configuration
22
NOTION_API_KEY=your_notion_api_key_here
3-
DATABASE_ID=your_database_id_here
4-
# DATA_SOURCE_ID is required for Notion API v5 (may be the same as DATABASE_ID for existing databases)
5-
# Run: bun scripts/migration/discoverDataSource.ts to find the correct value
3+
4+
# DATA_SOURCE_ID is the primary variable for Notion API v5 (version 2025-09-03)
5+
# This is the ID of the "data source" in Notion's new API architecture
6+
# Run the discovery script to find the correct value for your database:
7+
# bun scripts/migration/discoverDataSource.ts
68
DATA_SOURCE_ID=your_data_source_id_here
79

10+
# DATABASE_ID is kept for backward compatibility during migration
11+
# In Notion API v5, DATABASE_ID and DATA_SOURCE_ID may be DIFFERENT values
12+
# The DATABASE_ID fallback will be deprecated in a future release
13+
DATABASE_ID=your_database_id_here
14+
815
# Docusaurus Configuration
916
# Default landing page for docs redirect (e.g., 'introduction', 'introduction-remove', 'getting-started')
1017
DEFAULT_DOCS_PAGE=introduction-remove
@@ -23,3 +30,22 @@ ENABLE_RETRY_IMAGE_PROCESSING=true
2330
# Default: "3"
2431
# Recommendation: 3 attempts is optimal balance between recovery and performance
2532
MAX_IMAGE_RETRIES=3
33+
34+
# Test Environment Configuration
35+
# For translation workflow testing, you can use a dedicated test database
36+
# When TEST_DATABASE_ID or TEST_DATA_SOURCE_ID is set, translation scripts
37+
# will use the test database instead of the production database
38+
#
39+
# To enable test mode:
40+
# 1. Set TEST_DATABASE_ID or TEST_DATA_SOURCE_ID to your test database ID
41+
# 2. Optionally set TEST_MODE=true to explicitly enable test mode
42+
#
43+
# In test mode:
44+
# - All translation operations use the test database
45+
# - GitHub workflow dispatch validates target_branch against safe patterns
46+
# - Safe branches: "test/*", "fix/*", "feat/*", or branches with "test" in name
47+
# - Production branches ("main", "master", "content") are rejected in test mode
48+
#
49+
# Example:
50+
# TEST_DATA_SOURCE_ID=test-database-id-here
51+
# TEST_MODE=true

.github/workflows/translate-docs.yml

Lines changed: 84 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,29 @@ jobs:
9898
- name: Notion Translate
9999
id: notion_translate
100100
run: |
101-
# Use test database if test mode is enabled
102-
if [ -n "${{ secrets.TEST_DATA_SOURCE_ID }}" ] || [ -n "${{ secrets.TEST_DATABASE_ID }}" ] || [ "${{ secrets.TEST_MODE }}" = "true" ]; then
101+
# Check if any test mode indicator is set
102+
HAS_TEST_MODE="${{ secrets.TEST_MODE }}"
103+
HAS_TEST_DATA_SOURCE="${{ secrets.TEST_DATA_SOURCE_ID }}"
104+
HAS_TEST_DATABASE="${{ secrets.TEST_DATABASE_ID }}"
105+
106+
if [ "$HAS_TEST_MODE" = "true" ] || [ -n "$HAS_TEST_DATA_SOURCE" ] || [ -n "$HAS_TEST_DATABASE" ]; then
107+
echo "🧪 Test mode detected"
108+
109+
# Require at least one test ID to be set - fail if test mode is enabled without test IDs
110+
if [ -z "$HAS_TEST_DATA_SOURCE" ] && [ -z "$HAS_TEST_DATABASE" ]; then
111+
echo "❌ ERROR: Test mode enabled but no test IDs provided"
112+
echo " Set TEST_DATA_SOURCE_ID or TEST_DATABASE_ID to use test mode"
113+
exit 1
114+
fi
115+
103116
echo "🧪 Running in test mode with test database"
104-
export DATA_SOURCE_ID="${{ secrets.TEST_DATA_SOURCE_ID }}"
105-
export DATABASE_ID="${{ secrets.TEST_DATABASE_ID }}"
117+
# Only override if test secret is non-empty to avoid breaking with empty secrets
118+
if [ -n "$HAS_TEST_DATA_SOURCE" ]; then
119+
export DATA_SOURCE_ID="$HAS_TEST_DATA_SOURCE"
120+
fi
121+
if [ -n "$HAS_TEST_DATABASE" ]; then
122+
export DATABASE_ID="$HAS_TEST_DATABASE"
123+
fi
106124
export TEST_MODE="true"
107125
fi
108126
@@ -121,7 +139,7 @@ jobs:
121139
if: always()
122140
id: parse_summary
123141
run: |
124-
# Default values if TRANSLATION_SUMMARY not found
142+
# Default values if summary not found
125143
export TOTAL_PAGES="0"
126144
export PROCESSED_LANGS="0"
127145
export NEW_TRANSLATIONS="0"
@@ -132,8 +150,27 @@ jobs:
132150
export THEME_FAILURES="0"
133151
export HAS_FAILURES="false"
134152
135-
# Extract TRANSLATION_SUMMARY line from output
136-
if [ -f /tmp/translate_output.log ]; then
153+
# Prefer JSON file (more reliable than log parsing)
154+
# Fall back to log parsing for backward compatibility
155+
if [ -f translation-summary.json ]; then
156+
echo "Using translation-summary.json for summary parsing"
157+
TOTAL_PAGES=$(jq -r '.totalEnglishPages // 0' translation-summary.json)
158+
PROCESSED_LANGS=$(jq -r '.processedLanguages // 0' translation-summary.json)
159+
NEW_TRANSLATIONS=$(jq -r '.newTranslations // 0' translation-summary.json)
160+
UPDATED_TRANSLATIONS=$(jq -r '.updatedTranslations // 0' translation-summary.json)
161+
SKIPPED_TRANSLATIONS=$(jq -r '.skippedTranslations // 0' translation-summary.json)
162+
FAILED_TRANSLATIONS=$(jq -r '.failedTranslations // 0' translation-summary.json)
163+
CODE_JSON_FAILURES=$(jq -r '.codeJsonFailures // 0' translation-summary.json)
164+
THEME_FAILURES=$(jq -r '.themeFailures // 0' translation-summary.json)
165+
166+
# Determine if there were any failures
167+
TOTAL_FAILURES=$((FAILED_TRANSLATIONS + CODE_JSON_FAILURES + THEME_FAILURES))
168+
if [ "$TOTAL_FAILURES" -gt 0 ]; then
169+
HAS_FAILURES="true"
170+
fi
171+
elif [ -f /tmp/translate_output.log ]; then
172+
# Fallback: extract TRANSLATION_SUMMARY line from output (legacy)
173+
echo "Falling back to log parsing for summary"
137174
SUMMARY_LINE=$(grep "^TRANSLATION_SUMMARY " /tmp/translate_output.log | head -1 || echo "")
138175
139176
if [ -n "$SUMMARY_LINE" ]; then
@@ -152,7 +189,7 @@ jobs:
152189
THEME_FAILURES=$(echo "$JSON_PART" | jq -r '.themeFailures // "0"' 2>/dev/null || echo "0")
153190
154191
# Determine if there were any failures
155-
TOTAL_FAILURES=$$((FAILED_TRANSLATIONS + CODE_JSON_FAILURES + THEME_FAILURES))
192+
TOTAL_FAILURES=$((FAILED_TRANSLATIONS + CODE_JSON_FAILURES + THEME_FAILURES))
156193
if [ "$TOTAL_FAILURES" -gt 0 ]; then
157194
HAS_FAILURES="true"
158195
fi
@@ -185,7 +222,7 @@ jobs:
185222
echo " Has failures: $HAS_FAILURES"
186223
187224
- name: Update Notion Status → Auto Translation Generated
188-
if: success()
225+
if: success() && (steps.parse_summary.outputs.new_translations != '0' || steps.parse_summary.outputs.updated_translations != '0')
189226
run: |
190227
# Use test database if test mode is enabled
191228
if [ -n "${{ secrets.TEST_DATA_SOURCE_ID }}" ] || [ -n "${{ secrets.TEST_DATABASE_ID }}" ] || [ "${{ secrets.TEST_MODE }}" = "true" ]; then
@@ -225,12 +262,12 @@ jobs:
225262
PUSH_SUCCESS=false
226263
227264
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ "$PUSH_SUCCESS" = false ]; do
228-
CURRENT_ATTEMPT=$$((RETRY_COUNT + 1))
265+
CURRENT_ATTEMPT=$((RETRY_COUNT + 1))
229266
if PUSH_OUTPUT=$(git push origin "${TARGET_BRANCH}" 2>&1); then
230267
PUSH_SUCCESS=true
231268
echo "✅ Push succeeded on attempt $CURRENT_ATTEMPT"
232269
else
233-
RETRY_COUNT=$$((RETRY_COUNT + 1))
270+
RETRY_COUNT=$((RETRY_COUNT + 1))
234271
235272
# Check if error is due to non-fast-forward (push race condition)
236273
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
@@ -245,7 +282,7 @@ jobs:
245282
echo "Rebasing local changes on top of origin/${TARGET_BRANCH}..."
246283
if git rebase "origin/${TARGET_BRANCH}" 2>&1; then
247284
# Calculate exponential backoff: 2^N seconds (2s, 4s, 8s)
248-
BACKOFF_TIME=$$((2 ** RETRY_COUNT))
285+
BACKOFF_TIME=$((2 ** RETRY_COUNT))
249286
echo "Waiting ${BACKOFF_TIME}s before retry..."
250287
sleep $BACKOFF_TIME
251288
else
@@ -276,39 +313,38 @@ jobs:
276313
env:
277314
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
278315

279-
280-
# - name: Notify Slack
281-
# if: always()
282-
# uses: slackapi/slack-github-action@v2.1.1
283-
# with:
284-
# webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
285-
# webhook-type: incoming-webhook
286-
# payload: |
287-
# text: "*Translation sync*: ${{ job.status }}"
288-
# blocks:
289-
# - type: "section"
290-
# text:
291-
# type: "mrkdwn"
292-
# text: "*Translation sync*: ${{ job.status == 'success' && fromJSON('["✅"]')[0] || fromJSON('["❌"]')[0] }} *${{ job.status }}*\n_Updated locales committed to `${{ env.TARGET_BRANCH }}`_"
293-
# - type: "section"
294-
# text:
295-
# type: "mrkdwn"
296-
# text: |
297-
# *Translation Results:*
298-
# • Pages processed: ${{ steps.parse_summary.outputs.total_pages }}
299-
# • Languages: ${{ steps.parse_summary.outputs.processed_langs }}
300-
# • New translations: ${{ steps.parse_summary.outputs.new_translations }}
301-
# • Updated translations: ${{ steps.parse_summary.outputs.updated_translations }}
302-
# • Skipped: ${{ steps.parse_summary.outputs.skipped_translations }}
303-
# ${{ steps.parse_summary.outputs.has_failures == 'true' && '*Failures:*' || '' }}
304-
# ${{ steps.parse_summary.outputs.failed_translations != '0' && fromJSON('[" • Doc translation failures: "]')[0] || '' }}${{ steps.parse_summary.outputs.failed_translations }}${{ steps.parse_summary.outputs.failed_translations != '0' && fromJSON('["']')[0] || '' }}
305-
# ${{ steps.parse_summary.outputs.code_json_failures != '0' && fromJSON('[" • Code.json failures: "]')[0] || '' }}${{ steps.parse_summary.outputs.code_json_failures }}${{ steps.parse_summary.outputs.code_json_failures != '0' && fromJSON('["']')[0] || '' }}
306-
# ${{ steps.parse_summary.outputs.theme_failures != '0' && fromJSON('[" • Theme (navbar/footer) failures: "]')[0] || '' }}${{ steps.parse_summary.outputs.theme_failures }}${{ steps.parse_summary.outputs.theme_failures != '0' && fromJSON('["']')[0] || '' }}
307-
# - type: "section"
308-
# text:
309-
# type: "mrkdwn"
310-
# text: "Trigger: <https://github.com/${{ github.triggering_actor }}|${{ github.triggering_actor }}>\nRun: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>"
311-
# - type: "section"
312-
# text:
313-
# type: "mrkdwn"
314-
# text: "${{ steps.parse_summary.outputs.has_failures == 'true' || job.status != 'success' && '⚠️ *Attention required*: Review failures before re-running.' || 'ℹ️ Reminder: Review translations before publishing.' }}"
316+
- name: Notify Slack
317+
if: always()
318+
uses: slackapi/slack-github-action@v2.1.1
319+
with:
320+
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
321+
webhook-type: incoming-webhook
322+
payload: |
323+
text: "*Translation sync*: ${{ job.status }}"
324+
blocks:
325+
- type: "section"
326+
text:
327+
type: "mrkdwn"
328+
text: "*Translation sync*: ${{ job.status == 'success' && fromJSON('["✅"]')[0] || fromJSON('["❌"]')[0] }} *${{ job.status }}*\n_Updated locales committed to `${{ env.TARGET_BRANCH }}`_"
329+
- type: "section"
330+
text:
331+
type: "mrkdwn"
332+
text: |
333+
*Translation Results:*
334+
• Pages processed: ${{ steps.parse_summary.outputs.total_pages }}
335+
• Languages: ${{ steps.parse_summary.outputs.processed_langs }}
336+
• New translations: ${{ steps.parse_summary.outputs.new_translations }}
337+
• Updated translations: ${{ steps.parse_summary.outputs.updated_translations }}
338+
• Skipped: ${{ steps.parse_summary.outputs.skipped_translations }}
339+
${{ steps.parse_summary.outputs.has_failures == 'true' && '*Failures:*' || '' }}
340+
${{ steps.parse_summary.outputs.failed_translations != '0' && fromJSON('[" • Doc translation failures: "]')[0] || '' }}${{ steps.parse_summary.outputs.failed_translations }}${{ steps.parse_summary.outputs.failed_translations != '0' && fromJSON('["']')[0] || '' }}
341+
${{ steps.parse_summary.outputs.code_json_failures != '0' && fromJSON('[" • Code.json failures: "]')[0] || '' }}${{ steps.parse_summary.outputs.code_json_failures }}${{ steps.parse_summary.outputs.code_json_failures != '0' && fromJSON('["']')[0] || '' }}
342+
${{ steps.parse_summary.outputs.theme_failures != '0' && fromJSON('[" • Theme (navbar/footer) failures: "]')[0] || '' }}${{ steps.parse_summary.outputs.theme_failures }}${{ steps.parse_summary.outputs.theme_failures != '0' && fromJSON('["']')[0] || '' }}
343+
- type: "section"
344+
text:
345+
type: "mrkdwn"
346+
text: "Trigger: <https://github.com/${{ github.triggering_actor }}|${{ github.triggering_actor }}>\nRun: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>"
347+
- type: "section"
348+
text:
349+
type: "mrkdwn"
350+
text: "${{ ((steps.parse_summary.outputs.has_failures == 'true') || (job.status != 'success')) && '⚠️ *Attention required*: Review failures before re-running.' || 'ℹ️ Reminder: Review translations before publishing.' }}"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,6 @@ NEXT_STEPS.md
9090

9191
# Runtime metrics files
9292
retry-metrics.json
93+
94+
# Claude Code command history
95+
.claude/command-history.log

.gitleaks.toml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Gitleaks configuration for CoMapeo Docs
2+
# https://github.com/gitleaks/gitleaks
3+
4+
title = "gitleaks config for CoMapeo Docs"
5+
6+
# Extend default gitleaks config
7+
[extend]
8+
useDefault = true
9+
10+
# Custom rules for this project
11+
[[rules]]
12+
id = "notion-api-key"
13+
description = "Notion API Key"
14+
regex = '''(?i)ntn_[a-zA-Z0-9]{43,}'''
15+
tags = ["key", "notion"]
16+
17+
[[rules]]
18+
id = "openai-api-key"
19+
description = "OpenAI API Key"
20+
regex = '''sk-[a-zA-Z0-9]{20,}'''
21+
tags = ["key", "openai"]
22+
23+
[[rules]]
24+
id = "cloudflare-api-token"
25+
description = "Cloudflare API Token"
26+
regex = '''(?i)cloudflare[_-]?(api)?[_-]?token[_-]?[:=]\s*['\"]?([a-zA-Z0-9_-]{40,})['\"]?'''
27+
tags = ["key", "cloudflare"]
28+
29+
[[rules]]
30+
id = "generic-api-key"
31+
description = "Generic API Key"
32+
regex = '''(?i)(api[_-]?key|apikey)[_-]?[:=]\s*['\"]?([a-zA-Z0-9_-]{20,})['\"]?'''
33+
tags = ["key", "generic"]
34+
[rules.allowlist]
35+
# Allow .env.example file
36+
paths = [
37+
'''\.env\.example$''',
38+
]
39+
40+
# Global allowlist
41+
[allowlist]
42+
description = "Allowlist for safe files and patterns"
43+
paths = [
44+
'''\.env\.example$''',
45+
'''\.gitleaks\.toml$''',
46+
'''node_modules/''',
47+
'''\.git/''',
48+
'''docs/developer-tools/github-setup\.md$''', # Documentation may contain example patterns
49+
]
50+
51+
# Stop words that indicate example/placeholder values
52+
stopwords = [
53+
"your_notion_api_key_here",
54+
"your_data_source_id_here",
55+
"your_database_id_here",
56+
"your_openai_api_key_here",
57+
"REDACTED",
58+
"example",
59+
"sample",
60+
"placeholder",
61+
"fake",
62+
"test",
63+
]

0 commit comments

Comments
 (0)