diff --git a/.github/workflows/doc-orchestrator.yml b/.github/workflows/doc-orchestrator.yml new file mode 100644 index 00000000000..202c5aa03f9 --- /dev/null +++ b/.github/workflows/doc-orchestrator.yml @@ -0,0 +1,2744 @@ +# ๐Ÿฆฉ Flamingo AI Technical Writer (doc-orchestrator pipeline) +# =========================== +# This GitHub Actions workflow is triggered by the multi-platform-hub to generate +# comprehensive documentation for this repository and create a PR with the results. +# +# 4-Stage Pipeline: +# 1. Inline Documentation - Generate .md files next to source classes +# 2. CodeWiki Analysis - Architecture overview and Mermaid diagrams +# 3. AI Tutorial Generator - VoltAgent-powered tutorial generation with tool-based exploration +# 4. Repository Documentation - Generate/update README.md, CONTRIBUTING.md; copy LICENSE.md, SECURITY.md +# +# NOTE: Stage 3 uses the VoltAgent framework (https://voltagent.dev/) for agentic +# document generation with three tools: list_docs, read_doc_file, write_tutorial_doc. +# NOTE: Stage 4 uses VoltAgent to update README with OpenFrame structure while preserving existing content. +# +# Installation: +# 1. Copy this file to .github/workflows/doc-orchestrator.yml in your target repository +# 2. Configure these GitHub Secrets in the target repository (Settings > Secrets): +# - ANTHROPIC_API_KEY: For AI processing (Stage 1 & 3) +# - OPENAI_API_KEY: For CodeWiki (Stage 2) +# - DOC_ORCH_WEBHOOK_SECRET: For callback authentication +# - DOC_ORCH_GITHUB_PAT: (Optional) For private dependency access +# - YOUTUBE_API_KEY: (Optional) For YouTube video embedding in Stage 3 & 4 + +name: ๐Ÿฆฉ Flamingo AI Technical Writer + +on: + # Push trigger - registers workflow with GitHub Actions (required for workflow_dispatch API) + # Only triggers when the workflow file itself is modified (runs once on initial setup) + push: + paths: + - '.github/workflows/doc-orchestrator.yml' + + repository_dispatch: + types: [doc-orchestrator] + + workflow_dispatch: + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # GENERATED FROM SINGLE SOURCE OF TRUTH: lib/config/doc-orchestrator-params.ts + # This section is auto-generated at runtime when creating workflow PRs + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + inputs: + # Individual parameters + run_id: + description: 'Unique execution ID' + required: true + repo_id: + description: 'Repository ID from database' + required: true + hub_base_url: + description: 'Hub base URL (e.g., https://product-hub.flamingo.so)' + required: true + stages: + description: 'Pipeline stages to execute' + required: true + dependencies: + description: 'Comma-separated dependency repos' + required: false + default: '' + source_branch: + description: 'Branch to analyze code from' + required: true + source_files_limit: + description: 'Max source files to process (0 = unlimited)' + required: true + claude_model: + description: 'Claude model ID for Stage 1/3/4 + Stage 2 Claude-arch fallback (SSOT from hub)' + required: true + codewiki_config: + description: 'Complete CodeWiki configuration (per-phase models, engine, stages, depth)' + required: true + output_paths: + description: 'Output paths configuration' + required: true + timeout: + description: 'Timeout in hours' + required: true + youtube_config: + description: 'YouTube integration configuration (channels only - API key in secrets)' + required: true + readme_config: + description: 'README logo configuration' + required: true + custom_repo_instructions: + description: 'Custom AI instructions' + required: false + default: '' + external_repos: + description: 'External repos JSON' + required: false + default: '[]' + stage_count: + description: 'Total number of pipeline stages' + required: false + default: '4' + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # END GENERATED SECTION + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +env: + # SECURITY: Only NON-SENSITIVE variables in job-level env + # Secrets are passed per-step to avoid exposure in job setup logs + # Run configuration (non-sensitive) + RUN_ID: ${{ github.event.client_payload.run_id || github.event.inputs.run_id || github.run_id }} + REPO_ID: ${{ github.event.client_payload.repo_id || github.event.inputs.repo_id || '' }} + HUB_BASE_URL: ${{ github.event.client_payload.hub_base_url || github.event.inputs.hub_base_url || '' }} + # No literal fallback: the four-stage list lives in DOC_ORCHESTRATOR_STAGES on the + # hub and is lifted into the payload per repo. A literal here would silently + # restore all four stages on a payload gap โ€” and Clean Slate would still have + # wiped the docs tree first, so the run would delete docs and regenerate nothing. + STAGES: ${{ github.event.client_payload.stages || github.event.inputs.stages || '' }} + DEPENDENCIES: ${{ github.event.client_payload.dependencies || github.event.inputs.dependencies || '' }} + STAGE_COUNT: ${{ github.event.client_payload.stage_count || github.event.inputs.stage_count || '4' }} + # Branch to checkout for code analysis (github_branch from repo config) + SOURCE_BRANCH: ${{ github.event.client_payload.source_branch || github.event.inputs.source_branch || 'main' }} + # Debug/testing: limit total source files to analyze (0=unlimited) + # Files beyond this limit are DELETED - all stages then process remaining files + SOURCE_FILES_LIMIT: ${{ github.event.client_payload.source_files_limit || github.event.inputs.source_files_limit || '0' }} + # Claude model SSOT: passed in from hub's CLAUDE_MODELS.SONNET constant + # (see `lib/constants/ai-models.ts DOC_ORCHESTRATOR_DEFAULT_MODEL`). + # Stage 1/3/4 generator scripts + Stage 2 Claude-arch fallback all read + # this env var. NO literal model string is allowed in any shipped + # template script โ€” scripts throw if CLAUDE_MODEL is empty. Re-run + # "Setup Workflow" after bumping the hub-side constant to propagate. + CLAUDE_MODEL: ${{ github.event.client_payload.claude_model || github.event.inputs.claude_model || '' }} + # ============================================================================= + # JSON-grouped parameters to stay under GitHub Actions 25-parameter limit + # These are parsed early in the workflow to extract individual values + # ============================================================================= + # NOTE: these fall back to EMPTY, not '{}'. An empty-object default made the + # `[ -z ... ]` presence checks below unreachable, so a missing payload silently + # produced `null` for every jq lookup and propagated as `--cluster-model null`. + # The hub always sends a complete, deep-merged blob (buildPayloadFromRepo). + CODEWIKI_CONFIG_JSON: ${{ github.event.client_payload.codewiki_config || github.event.inputs.codewiki_config || '' }} + OUTPUT_PATHS_JSON: ${{ github.event.client_payload.output_paths || github.event.inputs.output_paths || '' }} + # Stage timeouts (in hours) โ€” single source of truth, downstream steps reference + # `env.STAGE_TIMEOUT_HOURS` directly. Stage 4 is the exception (1h vs 24h cap). + STAGE_TIMEOUT_HOURS: ${{ github.event.client_payload.timeout || github.event.inputs.timeout || '24' }} + # Aliases preserved for downstream step env: keys (they reference these names + # by string). All resolve to the same single source. + STAGE1_TIMEOUT_HOURS: ${{ github.event.client_payload.timeout || github.event.inputs.timeout || '24' }} + # Stage 1 incremental push: commit + push the PR branch every N generated inline + # docs. A 24h stage that gets cancelled used to lose ALL of its work because the + # only commit happened after the generator returned. Bound the loss to N files. + STAGE1_PUSH_INTERVAL: ${{ github.event.client_payload.stage1_push_interval || '100' }} + STAGE2_TIMEOUT_HOURS: ${{ github.event.client_payload.timeout || github.event.inputs.timeout || '24' }} + STAGE3_TIMEOUT_HOURS: ${{ github.event.client_payload.timeout || github.event.inputs.timeout || '24' }} + # Stage 4: Repository Documentation + TEMPLATE_REPO: ${{ github.event.client_payload.template_repo || 'flamingo-stack/openframe-oss-tenant' }} + TEMPLATE_BRANCH: ${{ github.event.client_payload.template_branch || 'main' }} + STAGE4_TIMEOUT_HOURS: ${{ github.event.client_payload.stage4_timeout || '1' }} + # YouTube Integration (Stage 3 + Stage 4) - JSONB configuration (API key from secrets) + YOUTUBE_CONFIG_JSON: ${{ github.event.client_payload.youtube_config || github.event.inputs.youtube_config || '' }} + # README Configuration - JSONB configuration for logo branding + README_CONFIG_JSON: ${{ github.event.client_payload.readme_config || github.event.inputs.readme_config || '' }} + # Custom AI Instructions (All Stages) - Repository-specific instructions for AI generation + CUSTOM_REPO_INSTRUCTIONS: ${{ github.event.client_payload.custom_repo_instructions || github.event.inputs.custom_repo_instructions || '' }} + # External Repositories - JSON array of external repo configurations + EXTERNAL_REPOS: ${{ github.event.client_payload.external_repos || github.event.inputs.external_repos || '[]' }} + # ======================================================================== + # Repository Context - CRITICAL for preventing AI URL hallucinations + # These values are passed to ALL AI prompts to ensure correct GitHub URLs + # ======================================================================== + GITHUB_REPOSITORY: ${{ github.repository }} # e.g., "flamingo-stack/openframe-oss-tenant" + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} # e.g., "flamingo-stack" + GITHUB_SERVER_URL: ${{ github.server_url }} # e.g., "https://github.com" + # Analysis Exclusions - Complete array of glob patterns to exclude from repository analysis + # Prevents analyzing orchestrator frontend, build artifacts, dependencies, generated docs + EXCLUDED_PATHS: '**/node_modules/**,**/multi-platform-hub/**,**/deps-*/**,**/target/**,**/dist/**,**/build/**,**/.next/**' + README_LOGO_ALT: 'OpenFrame Logo' + +jobs: + doc-pipeline: + runs-on: ubuntu-latest + timeout-minutes: 720 # 12 hours for large repositories with many files + # Skip actual work when triggered by push (push trigger only registers workflow with GitHub) + # This allows workflow_dispatch API calls to work on feature branches + if: github.event_name != 'push' + + steps: + # ========================================================================= + # DOWNLOAD WORKFLOW SCRIPTS + # Downloads all reusable scripts from authenticated admin-hub endpoint + # ========================================================================= + - name: Download Workflow Scripts + id: helpers + env: + WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} + # Script file SHA256 hashes for integrity verification (v2 rollout). + # 11 entries = 9 carried over from v1 (6 byte-identical, 3 recomputed) + 2 new helpers. + # Recomputed: HASH_INLINE_DOCS / HASH_TUTORIALS / HASH_REPO_DOCS shrink after + # the shared helpers (youtube-search-tool, prompt-context-loader) were extracted. + HASH_WORKFLOW_HELPERS: "11c1019fe69a24325d3849ea27bce73ec81591e3d6f6a72f3b3421cfb72551ea" + HASH_CODEWIKI_ANALYSIS: "2bfbd6c1776e0a1d28204bf4a412ff71be459e8b3d575a7ecac267f6c0824654" + HASH_CLAUDE_ANALYSIS: "216c206eb9329082207661f6084cbab03eae3270befc2dc88d547b3ea7a097b3" + HASH_INLINE_DOCS: "c5a4d558489762944a155cae29b9234dc238a07617ec704c26fef08b3270379a" + HASH_TUTORIALS: "9560a3e3f5bc07ee915926532f1504f5c73036b9200b8af3b41248b8c63729d3" + HASH_REPO_DOCS: "18dcc70c23c9077febd5810af613b62f05d1dc50e0b014803fbec4cd362cb52c" + HASH_VALIDATE_MARKDOWN: "30984fe265b23e1a6bd0fbcc4fd92a720bdbce9c949f73b01b4f7c86e46ae705" + HASH_VALIDATION_RULES: "337c9f13a91040db21b76f34c38472ccb0f1fb2b9aac5f4ba9445ab03de21ef2" + HASH_DETECT_ORPHANS: "de97754c36342e37a46bb423daf6ca687f1fcb12fba47f013d196df418d8b506" + # NEW helpers shared across Stage 1, Stage 3, and Stage 4 generators. + HASH_YOUTUBE_TOOL: "c349f4012cb2d2ccda41e3afcd366e0c57372f44c5640d2cac9dcd59efad4903" + HASH_PROMPT_LOADER: "92aaacddeb1442a30e08baea798d18f0d32c057e24ced9106190566e739daf2a" + run: | + echo "๐Ÿ“ฅ Downloading workflow scripts..." + + # Construct URLs from hub base URL โ€” see /api/doc-orchestrator/scripts/[name] route + SCRIPTS_BASE_URL="${HUB_BASE_URL}/api/doc-orchestrator/scripts" + CALLBACK_URL="${HUB_BASE_URL}/api/doc-orchestrator/webhook" + + # Function to download and verify script + download_and_verify() { + local script_name="$1" + local expected_hash="$2" + local output_path="/tmp/$script_name" + + curl -fsSL "$SCRIPTS_BASE_URL/$script_name" \ + -H "Authorization: Bearer $WEBHOOK_SECRET" \ + -o "$output_path" + + local actual_hash=$(shasum -a 256 "$output_path" | cut -d' ' -f1) + + if [ "$actual_hash" != "$expected_hash" ]; then + echo "โŒ HASH MISMATCH for $script_name!" + echo " Expected: $expected_hash" + echo " Actual: $actual_hash" + echo " This could indicate tampering or an outdated hash." + exit 1 + fi + + # Make shell scripts executable + if [[ "$script_name" == *.sh ]]; then + chmod +x "$output_path" + fi + + echo "โœ… $script_name verified (hash: ${actual_hash:0:16}...)" + } + + # Download and verify all scripts (v2) + # Shared helpers download FIRST so the generators that `require()` them + # at /tmp/* can find them when Node parses the files. + download_and_verify "prompt-context-loader.cjs" "$HASH_PROMPT_LOADER" + download_and_verify "youtube-search-tool.cjs" "$HASH_YOUTUBE_TOOL" + download_and_verify "workflow-helpers.sh" "$HASH_WORKFLOW_HELPERS" + download_and_verify "run-codewiki-analysis.sh" "$HASH_CODEWIKI_ANALYSIS" + download_and_verify "run-claude-architecture-analysis.sh" "$HASH_CLAUDE_ANALYSIS" + # v2: generate-inline-docs is now .cjs (converted from .js) so it can require() the loader. + download_and_verify "generate-inline-docs.cjs" "$HASH_INLINE_DOCS" + download_and_verify "generate-tutorials-voltagent.cjs" "$HASH_TUTORIALS" + download_and_verify "generate-repo-docs.cjs" "$HASH_REPO_DOCS" + download_and_verify "validate-markdown.js" "$HASH_VALIDATE_MARKDOWN" + download_and_verify "markdown-validation-rules.md" "$HASH_VALIDATION_RULES" + download_and_verify "detect-orphans.sh" "$HASH_DETECT_ORPHANS" + + echo "๐Ÿ“ฆ All 11 workflow files downloaded and verified (10 scripts + 1 ruleset)" + + # Export paths for all stages (use os.tmpdir() compatible paths) + echo "VALIDATION_RULES_PATH=/tmp/markdown-validation-rules.md" >> $GITHUB_ENV + echo "GUIDELINES_PATH=/tmp/flamingo-markdown-guidelines.md" >> $GITHUB_ENV + echo "STAGE3_FILES_TRACKER=/tmp/stage3-files.txt" >> $GITHUB_ENV + echo "STAGE3_STATS_FILE=/tmp/.doc-stage3-stats.json" >> $GITHUB_ENV + echo "STAGE4_FILES_TRACKER=/tmp/stage4-files.txt" >> $GITHUB_ENV + + # Download Flamingo Markdown Guidelines (separate endpoint) + # REQUIRED: Guidelines are needed for markdown validation and CodeWiki prompts + echo "" + echo "๐Ÿ“‹ Downloading Flamingo Markdown Guidelines..." + GUIDELINES_URL="${HUB_BASE_URL}/api/doc-orchestrator/guidelines" + HTTP_CODE=$(curl -fsSL -w "%{http_code}" \ + "$GUIDELINES_URL" \ + -H "Authorization: Bearer $WEBHOOK_SECRET" \ + -o "/tmp/flamingo-markdown-guidelines.md" 2>/dev/null) || HTTP_CODE="failed" + + if [ "$HTTP_CODE" = "200" ]; then + GUIDELINES_SIZE=$(wc -c < /tmp/flamingo-markdown-guidelines.md | tr -d ' ') + if [ "$GUIDELINES_SIZE" -lt 100 ]; then + echo "โŒ Guidelines file too small ($GUIDELINES_SIZE bytes) - likely an error response" + cat /tmp/flamingo-markdown-guidelines.md + exit 1 + fi + echo "โœ… Guidelines downloaded ($GUIDELINES_SIZE bytes)" + else + echo "โŒ Guidelines download failed (HTTP $HTTP_CODE)" + echo " URL: $GUIDELINES_URL" + echo " Guidelines are required for markdown validation and CodeWiki prompts." + echo " Check that the guidelines endpoint is deployed and working." + rm -f /tmp/flamingo-markdown-guidelines.md + exit 1 + fi + + # ========================================================================= + # SEND START NOTIFICATION + # Notify orchestrator that workflow has started running + # ========================================================================= + - name: Send Start Notification + if: env.HUB_BASE_URL != '' + continue-on-error: true + env: + WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} + run: | + source /tmp/workflow-helpers.sh + + # Construct callback URL from hub base URL + CALLBACK_URL="${HUB_BASE_URL}/api/doc-orchestrator/webhook" + + echo "๐Ÿ“ค Sending start notification to: $CALLBACK_URL" + + PAYLOAD="{ + \"run_id\": \"$RUN_ID\", + \"repo_id\": \"$REPO_ID\", + \"status\": \"running\", + \"workflow_run_id\": ${{ github.run_id }}, + \"workflow_url\": \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\", + \"current_stage\": \"inline-docs\" + }" + + HTTP_CODE=$(send_webhook "$CALLBACK_URL" "$WEBHOOK_SECRET" "$PAYLOAD" "/tmp/webhook_start_response.txt") || HTTP_CODE="failed" + + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then + echo "โœ… Start notification sent (HTTP $HTTP_CODE)" + else + echo "โš ๏ธ Start notification returned HTTP $HTTP_CODE (non-blocking)" + fi + + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ env.SOURCE_BRANCH }} + + # ========================================================================= + # SETUP: Clone Dependency Repos (if configured) + # ========================================================================= + - name: Clone Dependency Repositories + if: env.DEPENDENCIES != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # SECURITY: Pass secret per-step with inline masking + GITHUB_PAT: ${{ secrets.DOC_ORCH_GITHUB_PAT }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ฆ Cloning dependency repositories for documentation context..." + echo " Dependencies: $DEPENDENCIES" + ensure_directory "../deps" + + # Determine which token to use (PAT preferred for cross-repo access) + if [ -n "$GITHUB_PAT" ]; then + echo " Using provided GitHub PAT for cross-repo access" + CLONE_TOKEN="$GITHUB_PAT" + else + echo " Using default GITHUB_TOKEN (may not work for private cross-repo)" + CLONE_TOKEN="$GH_TOKEN" + fi + + # Configure git to use token for private repos + git config --global url."https://x-access-token:${CLONE_TOKEN}@github.com/".insteadOf "https://github.com/" + + IFS=',' read -ra DEPS <<< "$DEPENDENCIES" + for dep in "${DEPS[@]}"; do + repo_name=$(basename $dep) + echo "" + echo " ๐Ÿ“ Cloning: $dep โ†’ ../deps/$repo_name" + if git clone --depth 1 "https://github.com/$dep.git" "../deps/$repo_name" 2>&1; then + file_count=$(find "../deps/$repo_name" -type f \( -name "*.java" -o -name "*.ts" -o -name "*.py" -o -name "*.rs" \) 2>/dev/null | wc -l | tr -d ' ') + echo " โœ… Cloned successfully ($file_count source files)" + else + echo " โš ๏ธ Failed to clone $dep" + echo " If private, pass github_pat with 'repo' scope" + fi + done + + echo "" + echo "๐Ÿ“‚ Dependency directories:" + ls -la ../deps/ 2>/dev/null || echo " No dependencies cloned" + echo "" + echo "๐Ÿ“Š Total dependency source files available for documentation:" + find ../deps -type f \( -name "*.java" -o -name "*.ts" -o -name "*.py" -o -name "*.rs" \) 2>/dev/null | wc -l | xargs echo " " + + # ========================================================================= + # LANGUAGE DETECTION (runs before all stages for consistency) + # Determines primary language for filtering in Stage 1, 2, and 3 + # ========================================================================= + - name: Detect Repository Language + id: detect_language + run: | + source /tmp/workflow-helpers.sh + + # This step is the FIRST reader of CODEWIKI_CONFIG_JSON โ€” it runs before + # "Validate and Parse Input Parameters" โ€” so the emptiness guard lives here, + # ahead of the first jq, rather than in the later validation step. + if [ -z "$CODEWIKI_CONFIG_JSON" ]; then + echo "โŒ Missing: CODEWIKI_CONFIG_JSON (required before language detection)" + exit 1 + fi + + echo "๐Ÿ” Detecting repository primary language..." + echo " Scanning main repo (.) and dependency repos (../deps/)" + + # Count source files by extension (main repo + dependencies) + # Uses same exclusions as Stage 1 and Stage 2 for consistency + GO_COUNT=$(find . ../deps 2>/dev/null -name "*.go" -type f \ + -not -path "*/node_modules/*" -not -path "*/vendor/*" \ + -not -path "*/.git/*" -not -path "*/target/*" \ + -not -name "*_test.go" | wc -l | tr -d ' ') + RUST_COUNT=$(find . ../deps 2>/dev/null -name "*.rs" -type f \ + -not -path "*/target/*" -not -path "*/.git/*" | wc -l | tr -d ' ') + PY_COUNT=$(find . ../deps 2>/dev/null -name "*.py" -type f \ + -not -path "*/.venv/*" -not -path "*/venv/*" -not -path "*/.git/*" | wc -l | tr -d ' ') + JAVA_COUNT=$(find . ../deps 2>/dev/null -name "*.java" -type f \ + -not -path "*/.git/*" | wc -l | tr -d ' ') + TS_COUNT=$(find . ../deps 2>/dev/null \( -name "*.ts" -o -name "*.tsx" \) -type f \ + -not -path "*/node_modules/*" -not -path "*/.git/*" \ + -not -name "*.test.*" -not -name "*.spec.*" | wc -l | tr -d ' ') + JS_COUNT=$(find . ../deps 2>/dev/null \( -name "*.js" -o -name "*.jsx" \) -type f \ + -not -path "*/node_modules/*" -not -path "*/.git/*" \ + -not -name "*.test.*" -not -name "*.spec.*" | wc -l | tr -d ' ') + C_COUNT=$(find . ../deps 2>/dev/null \( -name "*.c" -o -name "*.cpp" -o -name "*.h" \) -type f \ + -not -path "*/.git/*" | wc -l | tr -d ' ') + CS_COUNT=$(find . ../deps 2>/dev/null -name "*.cs" -type f \ + -not -path "*/.git/*" | wc -l | tr -d ' ') + HCL_COUNT=$(find . ../deps 2>/dev/null -name "*.tf" -type f \ + -not -path "*/.terraform/*" -not -path "*/.git/*" | wc -l | tr -d ' ') + + echo "" + echo "๐Ÿ“Š File counts (excluding tests and generated files):" + echo " Go: $GO_COUNT" + echo " Rust: $RUST_COUNT" + echo " Python: $PY_COUNT" + echo " Java: $JAVA_COUNT" + echo " TypeScript: $TS_COUNT" + echo " JavaScript: $JS_COUNT" + echo " C/C++: $C_COUNT" + echo " C#: $CS_COUNT" + echo " HCL/Terraform: $HCL_COUNT" + + # Determine primary language using helper function + RESULT=$(find_primary_language "$GO_COUNT" "$RUST_COUNT" "$PY_COUNT" "$JAVA_COUNT" "$TS_COUNT" "$JS_COUNT" "$C_COUNT" "$CS_COUNT" "$HCL_COUNT") + PRIMARY_LANG="${RESULT%:*}" + MAX_COUNT="${RESULT#*:}" + + # Determine if CodeWiki supports this language + MIN_FILES_THRESHOLD=10 + + if [ "$JAVA_COUNT" -ge "$MIN_FILES_THRESHOLD" ] || \ + [ "$TS_COUNT" -ge "$MIN_FILES_THRESHOLD" ] || \ + [ "$JS_COUNT" -ge "$MIN_FILES_THRESHOLD" ] || \ + [ "$PY_COUNT" -ge "$MIN_FILES_THRESHOLD" ] || \ + [ "$C_COUNT" -ge "$MIN_FILES_THRESHOLD" ] || \ + [ "$CS_COUNT" -ge "$MIN_FILES_THRESHOLD" ]; then + CODEWIKI_SUPPORTED="true" + echo "" + echo "โœ… Primary language: $PRIMARY_LANG ($MAX_COUNT files)" + echo " CodeWiki supported: YES" + else + CODEWIKI_SUPPORTED="false" + echo "" + echo "โœ… Primary language: $PRIMARY_LANG ($MAX_COUNT files)" + echo " CodeWiki supported: NO (will use Claude Architecture Analysis)" + fi + + # Per-repo engine override. + # + # The detection above cannot see mixed repos: it counts `.` AND `../deps`, so + # a Rust or Go product with a TypeScript dependency clones its way past the + # >=10 threshold and runs CodeWiki over a codebase whose analyzers do not + # exist โ€” which yields synthetic module_1/module_2/... docs that look like a + # successful run. `engine` pins the choice. + # + # Applied here, before set_output, so all five downstream gates keep reading + # one value and need no change. It cannot live in the `if:` conditions: + # GitHub Actions expressions have no ternary. + CODEWIKI_ENGINE=$(require_json_key "$CODEWIKI_CONFIG_JSON" '.engine' 'codewiki engine') || exit 1 + case "$CODEWIKI_ENGINE" in + claude) + CODEWIKI_SUPPORTED="false" + echo " โš™๏ธ engine=claude โ€” forcing Claude Architecture Analysis" + ;; + codewiki) + CODEWIKI_SUPPORTED="true" + echo " โš™๏ธ engine=codewiki โ€” forcing CodeWiki" + ;; + auto) + echo " โš™๏ธ engine=auto โ€” using detected value ($CODEWIKI_SUPPORTED)" + ;; + *) + echo "โŒ Invalid engine '$CODEWIKI_ENGINE' (expected auto|codewiki|claude)" + exit 1 + ;; + esac + + # Output for use by subsequent steps + set_output "primary_language" "$PRIMARY_LANG" + set_output "codewiki_supported" "$CODEWIKI_SUPPORTED" + set_output "file_count" "$MAX_COUNT" + + # ========================================================================= + # VALIDATE AND PARSE ALL INPUT PARAMETERS + # 1. Validate all required parameters are present + # 2. Parse ALL JSON configurations into individual environment variables + # 3. Validate parsed values + # Required to stay under GitHub Actions 25-parameter limit + # ========================================================================= + - name: Validate and Parse Input Parameters + run: | + echo "๐Ÿ” Validating and parsing all input parameters..." + echo "" + + # =================================================================== + # 1. VALIDATE REQUIRED PARAMETERS + # =================================================================== + echo "1๏ธโƒฃ Validating required parameters..." + + VALIDATION_FAILED=0 + + # Core parameters + [ -z "$RUN_ID" ] && echo "โŒ Missing: RUN_ID" && VALIDATION_FAILED=1 + [ -z "$REPO_ID" ] && echo "โŒ Missing: REPO_ID" && VALIDATION_FAILED=1 + [ -z "$HUB_BASE_URL" ] && echo "โŒ Missing: HUB_BASE_URL" && VALIDATION_FAILED=1 + + [ -z "$STAGES" ] && echo "โŒ Missing: STAGES" && VALIDATION_FAILED=1 + [ -z "$CLAUDE_MODEL" ] && echo "โŒ Missing: CLAUDE_MODEL" && VALIDATION_FAILED=1 + + # JSON parameters + [ -z "$CODEWIKI_CONFIG_JSON" ] && echo "โŒ Missing: CODEWIKI_CONFIG_JSON" && VALIDATION_FAILED=1 + [ -z "$OUTPUT_PATHS_JSON" ] && echo "โŒ Missing: OUTPUT_PATHS_JSON" && VALIDATION_FAILED=1 + [ -z "$README_CONFIG_JSON" ] && echo "โŒ Missing: README_CONFIG_JSON" && VALIDATION_FAILED=1 + [ -z "$YOUTUBE_CONFIG_JSON" ] && echo "โŒ Missing: YOUTUBE_CONFIG_JSON" && VALIDATION_FAILED=1 + + if [ $VALIDATION_FAILED -eq 1 ]; then + echo "" + echo "โŒ Validation failed: Missing required parameters" + exit 1 + fi + + echo " โœ… All required parameters present" + echo "" + + # =================================================================== + # 2. PARSE CODEWIKI CONFIGURATION (nested JSONB structure) + # =================================================================== + echo "2๏ธโƒฃ Parsing CodeWiki configuration..." + + # Only the keys with a real consumer are extracted here โ€” the per-phase + # base_url / api_version / temperature / temperature_supported are read + # directly from CODEWIKI_CONFIG_JSON by configure_codewiki_from_json, which + # is the single place that builds the `codewiki config set` command. They + # used to be parsed here as well and exported to $GITHUB_ENV, where nothing + # read them. + # + # No `// default` fallbacks anywhere below. The hub deep-merges every JSON + # param against the params SSOT before dispatch, so an absent key is a real + # bug โ€” and a fallback here would silently win over the SSOT, which is how + # docs/architecture and docs/reference/architecture drifted apart. + # `require_json_key` / `optional_json_key` come from workflow-helpers.sh. + source /tmp/workflow-helpers.sh + CW_JSON="$CODEWIKI_CONFIG_JSON" + + # Parse nested cluster config + CODEWIKI_CLUSTER_PROVIDER=$(require_json_key "$CW_JSON" '.cluster.provider' 'cluster provider') || exit 1 + CODEWIKI_CLUSTER_MODEL=$(require_json_key "$CW_JSON" '.cluster.model' 'cluster model') || exit 1 + CODEWIKI_CLUSTER_MAX_TOKENS=$(require_json_key "$CW_JSON" '.cluster.max_tokens' 'cluster max_tokens') || exit 1 + CODEWIKI_CLUSTER_MAX_TOKEN_FIELD=$(require_json_key "$CW_JSON" '.cluster.max_token_field' 'cluster max_token_field') || exit 1 + # Nullable by design: api_version is null for every OpenAI model. + + # Parse nested generation config + CODEWIKI_GENERATION_PROVIDER=$(require_json_key "$CW_JSON" '.generation.provider' 'generation provider') || exit 1 + CODEWIKI_GENERATION_MODEL=$(require_json_key "$CW_JSON" '.generation.model' 'generation model') || exit 1 + CODEWIKI_GENERATION_MAX_TOKENS=$(require_json_key "$CW_JSON" '.generation.max_tokens' 'generation max_tokens') || exit 1 + CODEWIKI_GENERATION_MAX_TOKEN_FIELD=$(require_json_key "$CW_JSON" '.generation.max_token_field' 'generation max_token_field') || exit 1 + + # Parse nested fallback config + CODEWIKI_FALLBACK_PROVIDER=$(require_json_key "$CW_JSON" '.fallback.provider' 'fallback provider') || exit 1 + CODEWIKI_FALLBACK_MODEL=$(require_json_key "$CW_JSON" '.fallback.model' 'fallback model') || exit 1 + CODEWIKI_FALLBACK_MAX_TOKENS=$(require_json_key "$CW_JSON" '.fallback.max_tokens' 'fallback max_tokens') || exit 1 + CODEWIKI_FALLBACK_MAX_TOKEN_FIELD=$(require_json_key "$CW_JSON" '.fallback.max_token_field' 'fallback max_token_field') || exit 1 + + # Parse top-level config. + # max_files_per_module is LIVE: this value reaches CodeWiki through the + # job-scoped $GITHUB_ENV write below, and upstream reads it in its + # empty-module-tree branch โ€” the branch Go/Rust/HCL repos land in. + CODEWIKI_MAX_FILES_PER_MODULE=$(require_json_key "$CW_JSON" '.max_files_per_module' 'max_files_per_module') || exit 1 + CODEWIKI_MAX_DEPTH=$(require_json_key "$CW_JSON" '.max_depth' 'max_depth') || exit 1 + CODEWIKI_REPO=$(require_json_key "$CW_JSON" '.repo' 'codewiki repo url') || exit 1 + + echo " Cluster (Phase 2): $CODEWIKI_CLUSTER_PROVIDER / $CODEWIKI_CLUSTER_MODEL (${CODEWIKI_CLUSTER_MAX_TOKEN_FIELD}, ${CODEWIKI_CLUSTER_MAX_TOKENS} tokens)" + echo " Generation (Phase 3+): $CODEWIKI_GENERATION_PROVIDER / $CODEWIKI_GENERATION_MODEL (${CODEWIKI_GENERATION_MAX_TOKEN_FIELD}, ${CODEWIKI_GENERATION_MAX_TOKENS} tokens)" + echo " Fallback: $CODEWIKI_FALLBACK_PROVIDER / $CODEWIKI_FALLBACK_MODEL (${CODEWIKI_FALLBACK_MAX_TOKEN_FIELD}, ${CODEWIKI_FALLBACK_MAX_TOKENS} tokens)" + echo " Max depth: $CODEWIKI_MAX_DEPTH" + echo " Max files/module: $CODEWIKI_MAX_FILES_PER_MODULE" + echo " โœ… CodeWiki parameters parsed" + echo "" + + # =================================================================== + # 3. PARSE YOUTUBE CONFIGURATION (JSONB structure) + # =================================================================== + echo "3๏ธโƒฃ Parsing YouTube configuration..." + + # Parse YouTube config from JSONB (channels only - API key from secrets) + # channels is legitimately optional: no channels == feature off + YOUTUBE_CHANNELS=$(echo "$YOUTUBE_CONFIG_JSON" | jq -c '.channels // []') + + # YouTube is enabled if channels array has items + YOUTUBE_ENABLED=$(echo "$YOUTUBE_CHANNELS" | jq -r 'if length > 0 then "true" else "false" end') + + echo " Enabled: $YOUTUBE_ENABLED (based on channels count)" + echo " Channels: $YOUTUBE_CHANNELS" + echo " API key source: secrets.YOUTUBE_API_KEY (SECURE - not in database)" + echo " โœ… YouTube configuration parsed" + echo "" + + # =================================================================== + # 4. PARSE README CONFIGURATION (JSONB structure) + # =================================================================== + echo "4๏ธโƒฃ Parsing README logo configuration..." + + # Parse README config from JSONB + README_LOGO_DARK=$(optional_json_key "$README_CONFIG_JSON" '.logo_dark') + README_LOGO_LIGHT=$(optional_json_key "$README_CONFIG_JSON" '.logo_light') + README_LOGO_ALT=$(require_json_key "$README_CONFIG_JSON" '.logo_alt' 'readme logo alt') || exit 1 + + echo " Dark logo: ${README_LOGO_DARK:-'(not set)'}" + echo " Light logo: ${README_LOGO_LIGHT:-'(not set)'}" + echo " Alt text: $README_LOGO_ALT" + echo " โœ… README logo configuration parsed" + echo "" + + # =================================================================== + # 5. PARSE OUTPUT PATHS CONFIGURATION (5 params) + # =================================================================== + echo "5๏ธโƒฃ Parsing output paths..." + + # Same fail-loud contract as the codewiki block. These fallbacks were the + # last surviving copy of the five paths, and `.reference` still said + # docs/architecture while the SSOT said docs/reference/architecture โ€” the + # very drift the SSOT was created to end. + OP_JSON="$OUTPUT_PATHS_JSON" + DOCS_OUTPUT_PATH=$(require_json_key "$OP_JSON" '.docs' 'docs output path') || exit 1 + REFERENCE_OUTPUT_PATH=$(require_json_key "$OP_JSON" '.reference' 'reference output path') || exit 1 + DIAGRAMS_OUTPUT_PATH=$(require_json_key "$OP_JSON" '.diagrams' 'diagrams output path') || exit 1 + GETTING_STARTED_OUTPUT_PATH=$(require_json_key "$OP_JSON" '.getting_started' 'getting-started output path') || exit 1 + DEVELOPMENT_OUTPUT_PATH=$(require_json_key "$OP_JSON" '.development' 'development output path') || exit 1 + + echo " Base docs: $DOCS_OUTPUT_PATH" + echo " Reference: $REFERENCE_OUTPUT_PATH" + echo " Diagrams: $DIAGRAMS_OUTPUT_PATH" + echo " Getting started: $GETTING_STARTED_OUTPUT_PATH" + echo " Development: $DEVELOPMENT_OUTPUT_PATH" + echo " โœ… 5 output path parameters parsed" + echo "" + + # =================================================================== + # 6. SET CUSTOM INSTRUCTIONS & EXTERNAL REPOS (separate parameters) + # =================================================================== + echo "6๏ธโƒฃ Setting custom instructions and external repos..." + + # =================================================================== + # 6a. BUILD REPOSITORY CONTEXT (prevents AI URL hallucinations) + # =================================================================== + # Extract repository information from GitHub context + GITHUB_REPO="${{ github.repository }}" + GITHUB_OWNER="${{ github.repository_owner }}" + GITHUB_SERVER="${{ github.server_url }}" + GITHUB_REPO_NAME=$(echo "$GITHUB_REPO" | cut -d'/' -f2) + GITHUB_REPO_URL="${GITHUB_SERVER}/${GITHUB_REPO}" + + # Build repository context section (injected into ALL AI prompts) + # Use printf for multi-line string (avoids YAML parsing issues with heredoc) + printf -v REPOSITORY_CONTEXT '%s\n' \ + '## REPOSITORY CONTEXT - GROUND TRUTH' \ + '' \ + '**CRITICAL:** This section provides the ACTUAL repository information. You MUST use these exact values when constructing GitHub URLs.' \ + '' \ + "- **Repository:** ${GITHUB_REPO}" \ + "- **Owner:** ${GITHUB_OWNER}" \ + "- **Repository Name:** ${GITHUB_REPO_NAME}" \ + "- **Repository URL:** ${GITHUB_REPO_URL}" \ + "- **Server:** ${GITHUB_SERVER}" \ + '' \ + '**MANDATORY RULES FOR GITHUB URLS:**' \ + "1. ALWAYS use the exact repository path: \`${GITHUB_REPO}\`" \ + '2. NEVER use placeholder URLs like "your-org", "example-org", or "mycompany"' \ + '3. NEVER infer repository owner from file contents or dependencies' \ + '4. NEVER use upstream/parent repository URLs (if this is a fork, use the fork URL)' \ + "5. When linking to code: \`${GITHUB_REPO_URL}/blob/main/path/to/file\`" \ + "6. When linking to clone: \`git clone ${GITHUB_REPO_URL}.git\`" \ + "7. When linking to issues/PRs: \`${GITHUB_REPO_URL}/issues\` or \`${GITHUB_REPO_URL}/pulls\`" \ + "8. When linking to releases: \`${GITHUB_REPO_URL}/releases\`" \ + '' \ + '**If you find yourself writing a GitHub URL, verify it matches the Repository URL above.**' \ + "**ESPECIALLY IN README.md and tutorials - All GitHub URLs MUST use ${GITHUB_REPO}**" \ + '' \ + '---' + + echo " Repository: $GITHUB_REPO" + echo " Repository URL: $GITHUB_REPO_URL" + echo " Repository context length: ${#REPOSITORY_CONTEXT} chars" + + # DEBUG: Print first 200 chars of repository context to verify it's set + echo " Repository context preview: ${REPOSITORY_CONTEXT:0:200}..." + + # =================================================================== + # 6b. PREPEND REPOSITORY CONTEXT TO CUSTOM INSTRUCTIONS + # =================================================================== + # Custom instructions come as plain text from user + # Read from the environment rather than interpolating into single quotes. + # GitHub Actions expression substitution runs BEFORE bash parses the line, so a + # single apostrophe anywhere in an admin's instructions used to terminate the + # string and kill the step with a syntax error. NOTE: never write a literal + # empty GitHub expression in this run block, even inside a comment โ€” Actions + # evaluates it pre-bash and the whole workflow fails to parse. + USER_CUSTOM_INSTRUCTIONS="$CUSTOM_REPO_INSTRUCTIONS" + + # Combine repository context + user custom instructions + # Repository context goes FIRST (highest priority in prompts) + if [ -n "$USER_CUSTOM_INSTRUCTIONS" ]; then + CUSTOM_INSTRUCTIONS="${REPOSITORY_CONTEXT}"$'\n\n'"${USER_CUSTOM_INSTRUCTIONS}" + else + CUSTOM_INSTRUCTIONS="$REPOSITORY_CONTEXT" + fi + + # External repos come as separate JSON array parameter + + EXTERNAL_REPOS_COUNT=$(echo "$EXTERNAL_REPOS" | jq '. | length' 2>/dev/null || echo "0") + echo " User instructions length: ${#USER_CUSTOM_INSTRUCTIONS} chars" + echo " Repository context length: ${#REPOSITORY_CONTEXT} chars" + echo " Total instructions length (with repo context): ${#CUSTOM_INSTRUCTIONS} chars" + echo " External repos: $EXTERNAL_REPOS_COUNT repos" + + # DEBUG: Print first 300 chars of CUSTOM_INSTRUCTIONS to verify repository context is included + echo " ===== CUSTOM_INSTRUCTIONS PREVIEW =====" + echo "${CUSTOM_INSTRUCTIONS:0:300}..." + echo " ========================================" + + echo " โœ… Repository context + custom instructions + external repos set" + echo "" + + # =================================================================== + # 5. VALIDATE PARSED VALUES + # =================================================================== + echo "5๏ธโƒฃ Validating parsed values..." + + # Validate providers + if [[ ! "$CODEWIKI_CLUSTER_PROVIDER" =~ ^(anthropic|openai)$ ]]; then + echo "โŒ Invalid cluster provider: $CODEWIKI_CLUSTER_PROVIDER" + exit 1 + fi + + if [[ ! "$CODEWIKI_GENERATION_PROVIDER" =~ ^(anthropic|openai)$ ]]; then + echo "โŒ Invalid generation provider: $CODEWIKI_GENERATION_PROVIDER" + exit 1 + fi + + if [[ ! "$CODEWIKI_FALLBACK_PROVIDER" =~ ^(anthropic|openai)$ ]]; then + echo "โŒ Invalid fallback provider: $CODEWIKI_FALLBACK_PROVIDER" + exit 1 + fi + + # Validate model names are not empty + [ -z "$CODEWIKI_CLUSTER_MODEL" ] && echo "โŒ Empty cluster model" && exit 1 + [ -z "$CODEWIKI_GENERATION_MODEL" ] && echo "โŒ Empty generation model" && exit 1 + [ -z "$CODEWIKI_FALLBACK_MODEL" ] && echo "โŒ Empty fallback model" && exit 1 + + # Validate numeric values + [[ ! "$CODEWIKI_CLUSTER_MAX_TOKENS" =~ ^[0-9]+$ ]] && echo "โŒ Invalid cluster max_tokens: $CODEWIKI_CLUSTER_MAX_TOKENS" && exit 1 + [[ ! "$CODEWIKI_GENERATION_MAX_TOKENS" =~ ^[0-9]+$ ]] && echo "โŒ Invalid generation max_tokens: $CODEWIKI_GENERATION_MAX_TOKENS" && exit 1 + [[ ! "$CODEWIKI_FALLBACK_MAX_TOKENS" =~ ^[0-9]+$ ]] && echo "โŒ Invalid fallback max_tokens: $CODEWIKI_FALLBACK_MAX_TOKENS" && exit 1 + [[ ! "$CODEWIKI_MAX_DEPTH" =~ ^[0-9]+$ ]] && echo "โŒ Invalid max_depth: $CODEWIKI_MAX_DEPTH" && exit 1 + [[ ! "$CODEWIKI_MAX_FILES_PER_MODULE" =~ ^[0-9]+$ ]] && echo "โŒ Invalid max_files_per_module: $CODEWIKI_MAX_FILES_PER_MODULE" && exit 1 + + echo " โœ… All parsed values are valid" + echo "" + + # =================================================================== + # 7. EXPORT TO GITHUB_ENV (makes values available to all steps) + # =================================================================== + echo "7๏ธโƒฃ Exporting to GITHUB_ENV..." + + # CodeWiki config (31 vars: cluster=9, generation=10, fallback=9, shared=3) + echo "CODEWIKI_CLUSTER_PROVIDER=$CODEWIKI_CLUSTER_PROVIDER" >> $GITHUB_ENV + echo "CODEWIKI_CLUSTER_MODEL=$CODEWIKI_CLUSTER_MODEL" >> $GITHUB_ENV + echo "CODEWIKI_CLUSTER_MAX_TOKENS=$CODEWIKI_CLUSTER_MAX_TOKENS" >> $GITHUB_ENV + echo "CODEWIKI_CLUSTER_MAX_TOKEN_FIELD=$CODEWIKI_CLUSTER_MAX_TOKEN_FIELD" >> $GITHUB_ENV + echo "CODEWIKI_GENERATION_PROVIDER=$CODEWIKI_GENERATION_PROVIDER" >> $GITHUB_ENV + echo "CODEWIKI_GENERATION_MODEL=$CODEWIKI_GENERATION_MODEL" >> $GITHUB_ENV + echo "CODEWIKI_GENERATION_MAX_TOKENS=$CODEWIKI_GENERATION_MAX_TOKENS" >> $GITHUB_ENV + echo "CODEWIKI_GENERATION_MAX_TOKEN_FIELD=$CODEWIKI_GENERATION_MAX_TOKEN_FIELD" >> $GITHUB_ENV + echo "CODEWIKI_FALLBACK_PROVIDER=$CODEWIKI_FALLBACK_PROVIDER" >> $GITHUB_ENV + echo "CODEWIKI_FALLBACK_MODEL=$CODEWIKI_FALLBACK_MODEL" >> $GITHUB_ENV + echo "CODEWIKI_FALLBACK_MAX_TOKENS=$CODEWIKI_FALLBACK_MAX_TOKENS" >> $GITHUB_ENV + echo "CODEWIKI_FALLBACK_MAX_TOKEN_FIELD=$CODEWIKI_FALLBACK_MAX_TOKEN_FIELD" >> $GITHUB_ENV + echo "CODEWIKI_MAX_FILES_PER_MODULE=$CODEWIKI_MAX_FILES_PER_MODULE" >> $GITHUB_ENV + echo "CODEWIKI_MAX_DEPTH=$CODEWIKI_MAX_DEPTH" >> $GITHUB_ENV + echo "CODEWIKI_REPO=$CODEWIKI_REPO" >> $GITHUB_ENV + + # YouTube config (2 vars - API key from secrets, not exported here) + echo "YOUTUBE_ENABLED=$YOUTUBE_ENABLED" >> $GITHUB_ENV + echo "YOUTUBE_CHANNELS=$YOUTUBE_CHANNELS" >> $GITHUB_ENV + + # README config (3 vars) + echo "README_LOGO_DARK=$README_LOGO_DARK" >> $GITHUB_ENV + echo "README_LOGO_LIGHT=$README_LOGO_LIGHT" >> $GITHUB_ENV + echo "README_LOGO_ALT=$README_LOGO_ALT" >> $GITHUB_ENV + + # Output paths (5 vars) + echo "DOCS_OUTPUT_PATH=$DOCS_OUTPUT_PATH" >> $GITHUB_ENV + echo "REFERENCE_OUTPUT_PATH=$REFERENCE_OUTPUT_PATH" >> $GITHUB_ENV + echo "DIAGRAMS_OUTPUT_PATH=$DIAGRAMS_OUTPUT_PATH" >> $GITHUB_ENV + echo "GETTING_STARTED_OUTPUT_PATH=$GETTING_STARTED_OUTPUT_PATH" >> $GITHUB_ENV + echo "DEVELOPMENT_OUTPUT_PATH=$DEVELOPMENT_OUTPUT_PATH" >> $GITHUB_ENV + + # Custom instructions (2 vars) + echo "CUSTOM_INSTRUCTIONS<> $GITHUB_ENV + echo "$CUSTOM_INSTRUCTIONS" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + echo "EXTERNAL_REPOS=$EXTERNAL_REPOS" >> $GITHUB_ENV + + echo " โœ… 40 values exported to GITHUB_ENV (28 CodeWiki + 2 YouTube + 3 README + 5 output_paths + 2 custom [with repo context])" + echo "" + + # =================================================================== + # 7. SUMMARY + # =================================================================== + echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + echo "โœ… VALIDATION AND PARSING COMPLETE" + echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + echo "๐Ÿ“Š Parameters validated: 5" + echo "๐Ÿ“ฆ JSON configurations parsed: 3" + echo "๐Ÿ”ข Individual values extracted: 18" + echo "โœ… All values exported and ready for workflow stages" + echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + + # ========================================================================= + # UNIFIED FILE DISCOVERY (Single Source of Truth) + # Discovers SOURCE CODE files and optionally DELETES everything else. + # When SOURCE_FILES_LIMIT > 0: + # 1. Keeps only N source files (.ts, .java, .py, etc.) + # 2. DELETES ALL other files in the repo (aggressive cleanup) + # Generated docs (inline .md) are created AFTER this step, so not affected. + # ========================================================================= + - name: Discover Source Files + id: discover_files + env: + SOURCE_FILES_LIMIT: ${{ env.SOURCE_FILES_LIMIT }} + # DOCS_OUTPUT_PATH is needed below so the find can exclude the + # generated docs tree (deleted by the Clean Slate step that runs + # AFTER discovery but BEFORE Stage 1 โ€” source-extension files + # under docs/ would otherwise be enumerated, then deleted, then + # cause ENOENT in Stage 1). + DOCS_OUTPUT_PATH: ${{ env.DOCS_OUTPUT_PATH }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ” Discovering source code files..." + echo " Scanning main repo (.) and dependency repos (../deps/)" + + # File paths (all in /tmp to avoid accidental commits) + SOURCE_FILES_LIST="/tmp/.doc-orchestrator-source-files.txt" + ALL_SOURCE_TEMP="/tmp/all_source_files_discovered.txt" + ALL_FILES_TEMP="/tmp/all_files_in_repo.txt" + FILES_TO_DELETE="/tmp/files_to_delete.txt" + + # 1. Find SOURCE CODE files only (with standard exclusions) + # IMPORTANT: exclude $DOCS_OUTPUT_PATH/* โ€” the "Clean Slate" step below + # does `rm -rf $DOCS_OUTPUT_PATH` BEFORE Stage 1 reads this list. If + # any source-extension file lives under the docs tree (Doxygen's + # `docs/doxygen/documentation.h`, Sphinx `_extensions/*.py`, etc.), + # it would be enumerated here, then deleted, then Stage 1 hits + # ENOENT trying to read it. This is the source-discovery / + # Clean-Slate / Stage-1 ordering bug โ€” exclusion is the targeted fix. + find . ../deps 2>/dev/null -type f \( \ + -name "*.ts" -o -name "*.tsx" \ + -o -name "*.js" -o -name "*.jsx" \ + -o -name "*.java" \ + -o -name "*.py" \ + -o -name "*.go" \ + -o -name "*.rs" \ + -o -name "*.c" -o -name "*.cpp" -o -name "*.h" \ + -o -name "*.cs" -o -name "*.tf" \ + \) \ + -not -path "./$DOCS_OUTPUT_PATH/*" \ + -not -path "*/node_modules/*" \ + -not -path "*/vendor/*" \ + -not -path "*/target/*" \ + -not -path "*/.git/*" \ + -not -path "*/dist/*" \ + -not -path "*/.next/*" \ + -not -path "*/build/*" \ + -not -path "*/__pycache__/*" \ + -not -path "*/.venv/*" \ + -not -path "*/venv/*" \ + -not -path "*/coverage/*" \ + -not -name "*_test.go" \ + -not -name "*.test.*" \ + -not -name "*.spec.*" \ + -not -name "*_test.ts" \ + -not -name "*_test.js" \ + -not -name "*.test.ts" \ + -not -name "*.test.js" \ + -not -name "*.spec.ts" \ + -not -name "*.spec.js" \ + | sort > "$ALL_SOURCE_TEMP" + + TOTAL_SOURCE=$(wc -l < "$ALL_SOURCE_TEMP" | tr -d ' ') + FILE_LIMIT="${SOURCE_FILES_LIMIT:-0}" + + echo " Found $TOTAL_SOURCE source code files" + + # Apply limit: keep N source files, DELETE EVERYTHING ELSE + if [ "$FILE_LIMIT" -gt 0 ]; then + echo "" + echo "โšก DEBUG MODE: Keeping only $FILE_LIMIT source files, deleting EVERYTHING else" + + # Keep first N source files + head -n "$FILE_LIMIT" "$ALL_SOURCE_TEMP" > "$SOURCE_FILES_LIST" + KEEPING=$(wc -l < "$SOURCE_FILES_LIST" | tr -d ' ') + + # 2. Find ALL files in the repo (except .git and workflow temp files) + find . ../deps 2>/dev/null -type f \ + -not -path "*/.git/*" \ + -not -path "*/.git" \ + -not -name ".doc-orchestrator-*" \ + -not -name ".doc-stage*" \ + | sort > "$ALL_FILES_TEMP" + + TOTAL_FILES=$(wc -l < "$ALL_FILES_TEMP" | tr -d ' ') + echo " Total files in repo: $TOTAL_FILES" + + # Build delete list: ALL files EXCEPT the ones we're keeping + # Also preserve workflow temp files (.doc-orchestrator-*, .doc-stage*) + > "$FILES_TO_DELETE" + while IFS= read -r file; do + # Skip workflow temp files we need to preserve + case "$file" in + ./.doc-orchestrator-*|./.doc-stage*) continue ;; + esac + # Check if this file is in our keep list + if ! grep -qxF "$file" "$SOURCE_FILES_LIST" 2>/dev/null; then + echo "$file" >> "$FILES_TO_DELETE" + fi + done < "$ALL_FILES_TEMP" + + DELETE_COUNT=$(wc -l < "$FILES_TO_DELETE" | tr -d ' ') + echo " Files to delete: $DELETE_COUNT" + + # Delete all files NOT in the keep list + DELETED_COUNT=0 + while IFS= read -r file_to_delete; do + if [ -f "$file_to_delete" ]; then + rm -f "$file_to_delete" + DELETED_COUNT=$((DELETED_COUNT + 1)) + fi + done < "$FILES_TO_DELETE" + + echo " โœ… Keeping: $KEEPING source files" + echo " ๐Ÿ—‘๏ธ Deleted: $DELETED_COUNT files" + + rm -f "$FILES_TO_DELETE" "$ALL_FILES_TEMP" + + # Aggressively prune directories (including those with only dotfiles) + echo " ๐Ÿงน Pruning directories..." + PRUNED_COUNT=0 + + # First, delete all dotfiles except in .git and workflow temp files (they prevent dir deletion) + find . -type f -name ".*" \ + -not -path "*/.git/*" \ + -not -name ".doc-orchestrator-*" \ + -not -name ".doc-stage*" \ + -delete 2>/dev/null || true + + # Multiple passes to handle nested empty directories + for i in 1 2 3 4 5 6 7 8 9 10; do + PASS_COUNT=0 + while IFS= read -r empty_dir; do + if [ -d "$empty_dir" ] && [ -z "$(ls -A "$empty_dir" 2>/dev/null)" ]; then + rmdir "$empty_dir" 2>/dev/null && PASS_COUNT=$((PASS_COUNT + 1)) + fi + done < <(find . -type d -empty 2>/dev/null | grep -v "^.$" | grep -v ".git") + PRUNED_COUNT=$((PRUNED_COUNT + PASS_COUNT)) + [ "$PASS_COUNT" -eq 0 ] && break + done + + if [ "$PRUNED_COUNT" -gt 0 ]; then + echo " ๐Ÿ—‘๏ธ Pruned $PRUNED_COUNT empty directories" + fi + + # Show what's left + echo "" + echo "๐Ÿ“‚ Remaining structure:" + find . -type d -not -path "*/.git/*" -not -path "*/.git" | head -20 + else + # No limit - keep all source files + cp "$ALL_SOURCE_TEMP" "$SOURCE_FILES_LIST" + fi + + # Cleanup temp file + rm -f "$ALL_SOURCE_TEMP" + + # Count remaining files + FILE_COUNT=$(count_source_files "$SOURCE_FILES_LIST") + MAIN_COUNT=$(count_main_repo_files "$SOURCE_FILES_LIST") + DEPS_COUNT=$(count_dependency_files "$SOURCE_FILES_LIST") + + echo "" + echo "๐Ÿ“Š Source files to analyze:" + echo " Total: $FILE_COUNT files" + echo " Main repo: $MAIN_COUNT files" + echo " Dependencies: $DEPS_COUNT files" + + # Show breakdown by language + echo "" + echo "๐Ÿ“‹ By language:" + for ext in ts tsx js jsx java py go rs c cpp h cs tf; do + EXT_COUNT=$(count_by_extension "$SOURCE_FILES_LIST" "$ext") + if [ "$EXT_COUNT" -gt 0 ]; then + echo " .$ext: $EXT_COUNT files" + fi + done + + # Show first 20 files for debugging + echo "" + echo "๐Ÿ“„ Sample files (first 20):" + head -20 "$SOURCE_FILES_LIST" | sed 's/^/ /' + + # Output for use by subsequent steps + set_output "source_file_count" "$FILE_COUNT" + set_output "source_files_list" "$SOURCE_FILES_LIST" + + # ========================================================================= + # CREATE PR BRANCH EARLY (Progressive PR Support) + # Creates PR branch and initial PR before any stages run, so each stage + # can commit its results immediately. This prevents data loss on timeouts. + # ========================================================================= + - name: Sanitize Branch Name for PR + id: branch-name-early + run: | + source /tmp/workflow-helpers.sh + # Replace colons and other invalid chars with hyphens for git branch name + SAFE_RUN_ID=$(echo "$RUN_ID" | sed 's/[:]/-/g' | sed 's/[^a-zA-Z0-9._-]/-/g') + set_output "safe_run_id" "$SAFE_RUN_ID" + echo "๐Ÿ“ Sanitized RUN_ID for branch: $SAFE_RUN_ID" + + - name: Create PR Branch + id: create-pr-branch + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Creating PR branch early for progressive commits..." + + # Use sanitized run ID for branch name. Branch, PR title, status file and + # commit messages all carry the product name: ๐Ÿฆฉ Flamingo AI Technical Writer. + SAFE_RUN_ID="${{ steps.branch-name-early.outputs.safe_run_id }}" + BRANCH_NAME="docs/flamingo-ai-technical-writer-$SAFE_RUN_ID" + + # Configure git + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Create and push empty branch + git checkout -b "$BRANCH_NAME" + + # Create initial commit to enable PR creation + echo "# ๐Ÿฆฉ Flamingo AI Technical Writer: Started" > .flamingo-ai-technical-writer-status.md + echo "" >> .flamingo-ai-technical-writer-status.md + echo "Run ID: $SAFE_RUN_ID" >> .flamingo-ai-technical-writer-status.md + echo "Status: In Progress" >> .flamingo-ai-technical-writer-status.md + echo "Started: $(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> .flamingo-ai-technical-writer-status.md + git add .flamingo-ai-technical-writer-status.md + git commit -m "docs: Initialize ๐Ÿฆฉ Flamingo AI Technical Writer run [skip ci]" + git push -u origin "$BRANCH_NAME" + + # Store branch name for later steps + set_output "branch_name" "$BRANCH_NAME" + + echo "โœ… PR branch created: $BRANCH_NAME" + + - name: Ensure PR Labels Exist + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "๐Ÿท๏ธ Ensuring required PR labels exist..." + + # Labels used for ๐Ÿฆฉ Flamingo AI Technical Writer PRs + LABELS=( + "documentation:A label for documentation-related PRs:#0075ca" + "automated:PRs created by automation/bots:#ededed" + "in-progress:Work in progress - not ready for merge:#fbca04" + ) + + for LABEL_DEF in "${LABELS[@]}"; do + LABEL_NAME=$(echo "$LABEL_DEF" | cut -d: -f1) + LABEL_DESC=$(echo "$LABEL_DEF" | cut -d: -f2) + LABEL_COLOR=$(echo "$LABEL_DEF" | cut -d: -f3 | sed 's/#//') + + # Check if label exists + if gh label list --json name --jq '.[].name' | grep -q "^${LABEL_NAME}$"; then + echo " โœ“ Label '$LABEL_NAME' already exists" + else + echo " + Creating label '$LABEL_NAME'..." + gh label create "$LABEL_NAME" \ + --description "$LABEL_DESC" \ + --color "$LABEL_COLOR" || true + fi + done + + echo "โœ… All required labels verified" + + - name: Create Initial PR + id: create-initial-pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH_NAME: ${{ steps.create-pr-branch.outputs.branch_name }} + RUN_ID_VAR: ${{ env.RUN_ID }} + REPO_NAME: ${{ github.repository }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Creating Pull Request for branch: $BRANCH_NAME" + + # Create PR body in a temp file (avoiding YAML parsing issues) + { + echo "๐Ÿฆฉ Flamingo AI Technical Writer: In Progress" + echo "" + echo "Run ID: $RUN_ID_VAR" + echo "Status: Running..." + echo "" + echo "This PR will be updated as each documentation stage completes." + echo "" + echo "Progress" + echo "- Stage 1 Inline Documentation - Starting..." + echo "- Stage 2 Architecture Analysis - Pending" + echo "- Stage 3 Tutorial Generation - Pending" + echo "- Stage 4 Repository Documentation - Pending" + echo "" + echo "Generated by ๐Ÿฆฉ Flamingo AI Technical Writer" + } > /tmp/pr-body.md + + # Create PR with gh CLI (works with existing branches) + PR_URL=$(gh pr create \ + --base "$DEFAULT_BRANCH" \ + --head "$BRANCH_NAME" \ + --title "[IN PROGRESS] ๐Ÿฆฉ Flamingo AI Technical Writer" \ + --body-file /tmp/pr-body.md \ + --label "documentation,automated,in-progress") + + # Extract PR number from URL + PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') + + echo "โœ… PR created: $PR_URL" + echo " PR Number: $PR_NUMBER" + + # Set outputs for later steps + set_output "pull-request-url" "$PR_URL" + set_output "pull-request-number" "$PR_NUMBER" + + # ========================================================================= + # CLEAN SLATE: REMOVE ENTIRE DOCS DIRECTORY + # Deletes ALL documentation before regeneration + # Ensures 100% fresh documentation with zero orphaned files + # All documentation will be regenerated by workflow stages + # ========================================================================= + - name: Clean Slate - Remove Regenerated Docs + env: + DOCS_OUTPUT_PATH: ${{ env.DOCS_OUTPUT_PATH }} + REFERENCE_OUTPUT_PATH: ${{ env.REFERENCE_OUTPUT_PATH }} + DIAGRAMS_OUTPUT_PATH: ${{ env.DIAGRAMS_OUTPUT_PATH }} + GETTING_STARTED_OUTPUT_PATH: ${{ env.GETTING_STARTED_OUTPUT_PATH }} + DEVELOPMENT_OUTPUT_PATH: ${{ env.DEVELOPMENT_OUTPUT_PATH }} + BRANCH_NAME: ${{ steps.create-pr-branch.outputs.branch_name }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ—‘๏ธ Clean Slate: Removing documentation this run will regenerate..." + echo "" + + # Recorded so a stage that ends up producing nothing can restore what was + # deleted on its behalf. Without it, a skipped stage turns the pull request + # into a net DELETION of existing documentation. + PRE_CLEAN_SHA=$(git rev-parse HEAD) + echo "PRE_CLEAN_SHA=$PRE_CLEAN_SHA" >> $GITHUB_ENV + echo " Pre-clean commit: $PRE_CLEAN_SHA" + + # SCOPED TO THE CONFIGURED STAGES. + # + # This used to `rm -rf $DOCS_OUTPUT_PATH` unconditionally. That was safe only + # while every repo ran all four stages. With per-repo `stages`, wiping the + # whole tree when Stage 2 is disabled means the reference architecture and + # diagrams are deleted and never rebuilt โ€” the pull request becomes a net + # DELETION of existing documentation. + CLEAN_TARGETS=() + if [[ "$STAGES" == *"codewiki"* ]]; then + CLEAN_TARGETS+=("$REFERENCE_OUTPUT_PATH" "$DIAGRAMS_OUTPUT_PATH") + fi + if [[ "$STAGES" == *"tutorials"* ]]; then + CLEAN_TARGETS+=("$GETTING_STARTED_OUTPUT_PATH" "$DEVELOPMENT_OUTPUT_PATH") + fi + + # DELETE_TARGETS is what actually gets rm -rf'd; CLEAN_TARGETS is what the + # restore step keys PER-STAGE. They differ only for a full wipe: we delete + # the whole tree (so orphaned files from a previous layout โ€” e.g. synthetic + # module_N dirs left by an earlier clustering-failure run โ€” cannot survive) + # but still RECORD the per-stage subtrees, so the restore leaves each + # subtree deleted iff its OWN stage produced output. + # + # Recording the blanket DOCS_OUTPUT_PATH instead (the old behaviour) made the + # restore treat docs/ as a single unit that is "safe to leave deleted" only + # once ALL FOUR stages complete โ€” but the restore runs right after Stage 2, + # so Stage 3/4 are never 'completed' yet, and it restored the ENTIRE pre-clean + # tree every time, undoing the wipe and resurrecting the orphaned module_N docs. + DELETE_TARGETS=("${CLEAN_TARGETS[@]}") + FULL_WIPE=false + SELECTED_COUNT=$(echo "$STAGES" | tr ',' '\n' | grep -c .) + if [ "$SELECTED_COUNT" -ge "${STAGE_COUNT:-4}" ]; then + FULL_WIPE=true + DELETE_TARGETS=("$DOCS_OUTPUT_PATH") + # Record every stage-owned subtree (NOT the blanket docs/) so the restore + # keys each subtree on its own stage instead of the all-four AND. + CLEAN_TARGETS=("$REFERENCE_OUTPUT_PATH" "$DIAGRAMS_OUTPUT_PATH" "$GETTING_STARTED_OUTPUT_PATH" "$DEVELOPMENT_OUTPUT_PATH") + fi + + echo " Stages: $STAGES" + echo " Full wipe: $FULL_WIPE" + echo " Targets: ${CLEAN_TARGETS[*]:-(none)}" + + # Recorded so the restore step iterates exactly what was removed โ€” the + # stage -> subtree map has ONE home, here. + { + echo "CLEAN_TARGETS_RECORD<<__EOT__" + for t in ${CLEAN_TARGETS[@]+"${CLEAN_TARGETS[@]}"}; do echo "$t"; done + echo "__EOT__" + } >> $GITHUB_ENV + echo "" + + # Count files before deletion (for reporting). Iterate DELETE_TARGETS โ€” + # the actual rm list (blanket docs/ on a full wipe, per-stage subtrees + # otherwise) โ€” not the restore-record CLEAN_TARGETS. + DELETED_FILES=0 + for target in ${DELETE_TARGETS[@]+"${DELETE_TARGETS[@]}"}; do + if [ -d "$target" ]; then + TARGET_FILES=$(find "$target" -type f | wc -l | tr -d ' ') + DELETED_FILES=$((DELETED_FILES + TARGET_FILES)) + echo " ๐Ÿ—‘๏ธ Removing $target ($TARGET_FILES files)..." + rm -rf "$target" + else + echo " โญ๏ธ $target does not exist (nothing to clean)" + fi + done + if [ ${#DELETE_TARGETS[@]} -eq 0 ]; then + echo " โญ๏ธ No stage owns a docs subtree โ€” nothing to clean" + fi + + # Recreate base directory + mkdir -p "$DOCS_OUTPUT_PATH" + echo " ๐Ÿ“ Recreated empty: $DOCS_OUTPUT_PATH" + + # Commit the deletion to git (so it shows in PR) + if [ "$DELETED_FILES" -gt 0 ]; then + echo "" + echo "๐Ÿ“ Committing Clean Slate deletion..." + git add -A + + # Check if there are staged changes + STAGED_COUNT=$(git diff --cached --name-only | wc -l | tr -d ' ') + + if [ "$STAGED_COUNT" -gt 0 ]; then + git commit -m "chore(docs): Clean slate - remove all documentation ($DELETED_FILES files) [skip ci]" + git push origin "$BRANCH_NAME" + echo " โœ… Committed deletion of $STAGED_COUNT files" + else + echo " โญ๏ธ No staged changes (docs already clean)" + fi + fi + + echo "" + echo "โœ… Clean slate complete - ready for fresh documentation generation" + echo "" + + # ========================================================================= + # STAGE 1: INLINE CLASS DOCUMENTATION + # Generate .md files next to each source class + # Uses discovered files from the unified file discovery step + # ========================================================================= + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install Stage 1 Dependencies + if: contains(env.STAGES, 'inline-docs') + # Install generator deps in an ISOLATED tree under RUNNER_TEMP, NOT the target + # repo. npm resolves against an empty package.json here, so a target repo's own + # peer conflicts (e.g. react-accessible-accordion vs react 18) can never make this + # fail. No --legacy-peer-deps / --no-save band-aids. Generators find these via the + # NODE_PATH set on the generate step (RUNNER_TEMP/doc-orch-deps/node_modules). + run: | + mkdir -p "$RUNNER_TEMP/doc-orch-deps" && cd "$RUNNER_TEMP/doc-orch-deps" + npm init -y >/dev/null 2>&1 + npm install @anthropic-ai/sdk@0.115.0 zod@3.25.76 glob@13.0.6 + + - name: Generate Inline Class Documentation + id: stage1 + if: contains(env.STAGES, 'inline-docs') + env: + # SECURITY: Pass secrets per-step with inline masking + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} + # Claude model SSOT โ€” see workflow env CLAUDE_MODEL block + CLAUDE_MODEL: ${{ env.CLAUDE_MODEL }} + DOCS_OUTPUT_PATH: ${{ env.DOCS_OUTPUT_PATH }} + # Stage timeout (in hours) + STAGE1_TIMEOUT_HOURS: ${{ env.STAGE1_TIMEOUT_HOURS }} + # Incremental commit + push every N generated docs (see workflow env) + STAGE1_PUSH_INTERVAL: ${{ env.STAGE1_PUSH_INTERVAL }} + BRANCH_NAME: ${{ steps.create-pr-branch.outputs.branch_name }} + STAGE1_PUSH_MARKER: /tmp/stage1-progress-pushes + # Unified file discovery result (single source of truth) + SOURCE_FILES_LIST: ${{ steps.discover_files.outputs.source_files_list }} + SOURCE_FILE_COUNT: ${{ steps.discover_files.outputs.source_file_count }} + # NODE_PATH to find modules from /tmp/ scripts + NODE_PATH: ${{ runner.temp }}/doc-orch-deps/node_modules + # Custom AI Instructions (All Stages) + CUSTOM_REPO_INSTRUCTIONS: ${{ env.CUSTOM_INSTRUCTIONS }} + # External Repositories + EXTERNAL_REPOS: ${{ env.EXTERNAL_REPOS }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Stage 1: Generating inline documentation..." + echo " Using unified file list: $SOURCE_FILES_LIST ($SOURCE_FILE_COUNT files)" + echo " Incremental push: every $STAGE1_PUSH_INTERVAL generated docs โ†’ $BRANCH_NAME" + + # Fresh marker: the generator appends one line per progress push, and the + # commit step below reads it to know work was already pushed. + rm -f "$STAGE1_PUSH_MARKER" + + # Run with unified timeout helper (12 hours default) + # Script already downloaded to /tmp/ in setup step + run_with_timeout "Stage 1" "$STAGE1_TIMEOUT_HOURS" node /tmp/generate-inline-docs.cjs || true + # Continue - don't fail the workflow, preserve partial results + + # Count generated files (hidden .*.md files) + INLINE_DOCS=$(find . -name ".*.md" -newer .git -type f -not -path "./node_modules/*" -not -path "./.git/*" | wc -l) + set_output "stage1_files" "$INLINE_DOCS" + set_output "stage1_status" "completed" + + # ========================================================================= + # COMMIT STAGE 1 RESULTS (Progressive PR) + # ========================================================================= + - name: Commit and Push Stage 1 Results + if: always() && steps.stage1.outputs.stage1_status == 'completed' + id: commit-stage1 + env: + BRANCH_NAME: ${{ steps.create-pr-branch.outputs.branch_name }} + STAGE_FILES: ${{ steps.stage1.outputs.stage1_files }} + STAGE1_PUSH_MARKER: /tmp/stage1-progress-pushes + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Committing Stage 1 (Inline Documentation) results..." + + # The generator already commits + pushes every N docs (STAGE1_PUSH_INTERVAL). + # Report how much landed that way; what's left here is the final partial batch. + if [ -s "$STAGE1_PUSH_MARKER" ]; then + PUSHED_BATCHES=$(wc -l < "$STAGE1_PUSH_MARKER" | tr -d ' ') + PUSHED_FILES=$(awk '{ sum += $1 } END { print sum + 0 }' "$STAGE1_PUSH_MARKER") + echo " Already pushed during generation: $PUSHED_FILES files in $PUSHED_BATCHES batches" + fi + + # Push any commits the generator made but could not push (transient push failure) + git push origin "HEAD:refs/heads/$BRANCH_NAME" 2>/dev/null || true + + # Stage all .md files generated by Stage 1 (hidden inline docs) + find . -name ".*.md" -type f \ + -not -path "./node_modules/*" \ + -not -path "./.git/*" \ + -exec git add -f {} \; 2>/dev/null || true + + # Check if there are changes + STAGED_COUNT=$(git diff --cached --name-only | wc -l) + + if [ "$STAGED_COUNT" -gt 0 ]; then + # Commit and push + git commit -m "docs: Stage 1 - Inline documentation ($STAGE_FILES files) [skip ci]" + git push origin "$BRANCH_NAME" + + echo "โœ… Committed and pushed $STAGED_COUNT files from Stage 1" + set_output "committed" "true" + elif [ -s "$STAGE1_PUSH_MARKER" ]; then + # Everything already landed via the incremental progress pushes + echo "โœ… Nothing left to commit: all Stage 1 files pushed during generation" + set_output "committed" "true" + else + echo "โš ๏ธ No Stage 1 files to commit" + set_output "committed" "false" + fi + + # ========================================================================= + # ORPHAN DETECTION: INLINE FILES ONLY + # Runs immediately after Stage 1 to clean up orphaned inline docs + # Orphan = .*.md file where source file was deleted + # ========================================================================= + - name: Clean Up Orphaned Inline Documentation Files + if: always() && steps.stage1.outputs.stage1_status == 'completed' + id: orphan-detection-inline + env: + BRANCH_NAME: ${{ steps.create-pr-branch.outputs.branch_name }} + run: | + source /tmp/workflow-helpers.sh + + # Run orphan detection script + bash /tmp/detect-orphans.sh || true + + # Check if orphans were deleted (script writes list to /tmp/orphaned-inline-files.txt) + DELETED_FILES_LIST="/tmp/orphaned-inline-files.txt" + + if [ -f "$DELETED_FILES_LIST" ] && [ -s "$DELETED_FILES_LIST" ]; then + DELETED_COUNT=$(wc -l < "$DELETED_FILES_LIST" | tr -d ' ') + echo "" + echo "๐Ÿ“ Committing $DELETED_COUNT orphan deletions..." + + # Only add the specific files that were deleted by the script + while IFS= read -r deleted_file; do + git add "$deleted_file" 2>/dev/null || true + done < "$DELETED_FILES_LIST" + + # Verify we have staged changes + STAGED_COUNT=$(git diff --cached --name-only | wc -l | tr -d ' ') + + if [ "$STAGED_COUNT" -gt 0 ]; then + git commit -m "chore(docs): Remove $DELETED_COUNT orphaned inline files [skip ci]" + git push origin "$BRANCH_NAME" + echo "โœ… Committed $STAGED_COUNT orphan deletions" + set_output "orphans_deleted" "$STAGED_COUNT" + else + echo "โš ๏ธ No changes to commit (files may have been already removed)" + set_output "orphans_deleted" "0" + fi + else + echo "โœ… No orphaned inline files detected" + set_output "orphans_deleted" "0" + fi + + - name: Update PR with Stage 1 Progress + if: always() && steps.commit-stage1.outputs.committed == 'true' && steps.create-initial-pr.outputs.pull-request-number + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.create-initial-pr.outputs.pull-request-number }} + STAGE1_FILES: ${{ steps.stage1.outputs.stage1_files }} + RUN_ID_VAR: ${{ env.RUN_ID }} + REPO_NAME: ${{ github.repository }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Updating PR description with Stage 1 results..." + + # Update PR body + PR_BODY="## ๐Ÿฆฉ Flamingo AI Technical Writer: In Progress + + **Run ID:** \`$RUN_ID_VAR\` + **Status:** ๐Ÿ”„ Running... + + This PR is being updated as each documentation stage completes. + + ### Progress + - โœ… Stage 1: Inline Documentation - Completed ($STAGE1_FILES files) + - โณ Stage 2: Architecture Analysis - Running... + - โฑ๏ธ Stage 3: Tutorial Generation - Pending + - โฑ๏ธ Stage 4: Repository Documentation - Pending + + --- + ๐Ÿฆฉ Generated by [Flamingo AI Technical Writer](https://flamingo.run)" + + gh pr edit "$PR_NUMBER" --body "$PR_BODY" + + echo "โœ… PR #$PR_NUMBER updated with Stage 1 progress" + + - name: Report Stage 1 Progress + if: always() && contains(env.STAGES, 'inline-docs') && env.HUB_BASE_URL != '' + continue-on-error: true + env: + WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} + WORKFLOW_RUN_ID: ${{ github.run_id }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + STAGE1_STATUS: ${{ steps.stage1.outputs.stage1_status || 'skipped' }} + STAGE1_FILES: ${{ steps.stage1.outputs.stage1_files || 0 }} + PR_URL: ${{ steps.create-initial-pr.outputs.pull-request-url }} + PR_NUMBER: ${{ steps.create-initial-pr.outputs.pull-request-number }} + run: | + source /tmp/workflow-helpers.sh + + # Construct callback URL from hub base URL + CALLBACK_URL="${HUB_BASE_URL}/api/doc-orchestrator/webhook" + + echo "๐Ÿ“ค Reporting Stage 1 (Inline Docs) completion with PR URL..." + report_stage_progress "$CALLBACK_URL" "$WEBHOOK_SECRET" "$RUN_ID" "$REPO_ID" \ + "$WORKFLOW_RUN_ID" "$WORKFLOW_URL" "codewiki" \ + "$STAGE1_STATUS" "$STAGE1_FILES" "" "0" "" "0" "" "0" \ + "$PR_URL" "$PR_NUMBER" + + # ========================================================================= + # STAGE 2: CODEWIKI ANALYSIS + # Generate architecture overview and module tree + # Language detection already ran before Stage 1 (uses steps.detect_language outputs) + # ========================================================================= + + - name: Setup Python 3.12 + if: contains(env.STAGES, 'codewiki') && steps.detect_language.outputs.codewiki_supported == 'true' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install CodeWiki + id: codewiki_install + if: contains(env.STAGES, 'codewiki') && steps.detect_language.outputs.codewiki_supported == 'true' + continue-on-error: true + env: + CODEWIKI_MAX_FILES_PER_MODULE: ${{ env.CODEWIKI_MAX_FILES_PER_MODULE }} + CODEWIKI_REPO: ${{ env.CODEWIKI_REPO }} + run: | + # Install keyrings.alt for headless keyring support in CI environments + # Install ipython to suppress "Mermaidjs magic function not available" warning + # Install colorama for CodeWiki colored terminal output + pip install keyrings.alt ipython colorama + + # Clone CodeWiki directly (no pip caching issues) + # Fixes baked into fork: + # - retries=3 for Pydantic AI agents (prevents "Tool exceeded max retries count of 1") + # - Synthetic module creation when clustering returns 0 modules (prevents context overflow) + # - 'children' key fix for synthetic modules + # - module_tree.json path fix (commit c1dfe5c) - loads from base docs dir, not nested module dir + # See: https://github.com/flamingo-stack/CodeWiki + # Extract repo URL from CODEWIKI_REPO (strip git+ prefix and @branch/commit suffix) + REPO_URL=$(echo "$CODEWIKI_REPO" | sed 's|^git+||' | sed 's|@[^@]*$||') + REF=$(echo "$CODEWIKI_REPO" | grep -o '@[^@]*$' | sed 's|^@||' || echo "main") + echo "๐Ÿ“ฆ Cloning CodeWiki from: $REPO_URL (ref: ${REF:-main})" + rm -rf /tmp/CodeWiki + + # Clone and checkout - handle both branches and commit hashes + if [[ "${REF}" =~ ^[0-9a-f]{7,40}$ ]]; then + # Commit hash - clone full repo and checkout specific commit + git clone "$REPO_URL" /tmp/CodeWiki + cd /tmp/CodeWiki && git checkout "${REF}" && cd - + else + # Branch name - shallow clone + git clone --depth 1 --branch "${REF:-main}" "$REPO_URL" /tmp/CodeWiki + fi + + echo " Commit: $(cd /tmp/CodeWiki && git rev-parse --short HEAD)" + + # Install from local clone (reliable, no caching) + echo "๐Ÿ“ฆ Installing CodeWiki from local clone..." + pip install --no-cache-dir /tmp/CodeWiki + + source /tmp/workflow-helpers.sh + set_output "codewiki_installed" "true" + + - name: Configure CodeWiki + id: codewiki_config + if: contains(env.STAGES, 'codewiki') && steps.detect_language.outputs.codewiki_supported == 'true' && steps.codewiki_install.outputs.codewiki_installed == 'true' + continue-on-error: true + env: + # SECURITY: Pass secrets per-step with inline masking + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + # Set keyring backend via env var (must be set before any keyring operations) + PYTHON_KEYRING_BACKEND: keyrings.alt.file.PlaintextKeyring + # Flamingo Markdown Guidelines path (needed for module import during config/validate) + FLAMINGO_MARKDOWN_GUIDELINES_PATH: /tmp/flamingo-markdown-guidelines.md + # OSS Tenant Structure: Stage 2 outputs (for clean slate deletion) + REFERENCE_OUTPUT_PATH: ${{ env.REFERENCE_OUTPUT_PATH }} + DIAGRAMS_OUTPUT_PATH: ${{ env.DIAGRAMS_OUTPUT_PATH }} + run: | + source /tmp/workflow-helpers.sh + + # === KEYRING CONFIGURATION FOR CI === + # CodeWiki stores API keys in system keyring. In CI (no GUI), we must: + # 1. Create keyring config to specify PlaintextKeyring backend + # 2. Create data directory for credential storage + # See: https://github.com/FSoft-AI4Code/CodeWiki - uses keyring.set_password() + + echo "๐Ÿ”‘ Setting up keyring for headless CI environment..." + + # Create keyring configuration directory and config file + mkdir -p ~/.config/python_keyring + cat > ~/.config/python_keyring/keyringrc.cfg << 'KEYRING_CFG' + [backend] + default-keyring=keyrings.alt.file.PlaintextKeyring + KEYRING_CFG + + # Ensure keyring data directory exists with proper permissions + mkdir -p ~/.local/share/python_keyring + chmod 700 ~/.local/share/python_keyring + + # Debug: Verify keyring is properly configured + echo "๐Ÿ“‹ Keyring backend verification:" + python3 -c "import keyring; print(f' Active backend: {keyring.get_keyring()}')" + + # Configure CodeWiki with separate cluster and generation providers/models + # CodeWiki calls provider APIs directly via --base-url + # Model names should match the provider's API format (no LiteLLM prefix needed) + # OpenAI: gpt-4o, gpt-4-turbo, gpt-4o-mini + # Provider ids come from MODEL_METADATA in lib/constants/ai-models.ts + # See: https://github.com/FSoft-AI4Code/CodeWiki + + echo "๐Ÿ”ง Configuring CodeWiki..." + echo " Cluster (Phase 2): $CODEWIKI_CLUSTER_PROVIDER / $CODEWIKI_CLUSTER_MODEL" + echo " Generation (Phase 3+): $CODEWIKI_GENERATION_PROVIDER / $CODEWIKI_GENERATION_MODEL" + + # Source helper functions for configuration + source /tmp/workflow-helpers.sh + + # Determine API keys for each provider (cluster, generation/main, fallback) + # Each provider can use a different AI service (OpenAI, Anthropic, etc.) + if [ "$CODEWIKI_CLUSTER_PROVIDER" = "anthropic" ]; then + CLUSTER_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" + echo " Cluster: Using Anthropic API key" + else + CLUSTER_API_KEY="${{ secrets.OPENAI_API_KEY }}" + echo " Cluster: Using OpenAI API key" + fi + + if [ "$CODEWIKI_GENERATION_PROVIDER" = "anthropic" ]; then + MAIN_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" + echo " Generation: Using Anthropic API key" + else + MAIN_API_KEY="${{ secrets.OPENAI_API_KEY }}" + echo " Generation: Using OpenAI API key" + fi + + if [ "$CODEWIKI_FALLBACK_PROVIDER" = "anthropic" ]; then + FALLBACK_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" + echo " Fallback: Using Anthropic API key" + else + FALLBACK_API_KEY="${{ secrets.OPENAI_API_KEY }}" + echo " Fallback: Using OpenAI API key" + fi + + # Configure CodeWiki from CODEWIKI_CONFIG_JSON (single extractor: configure_codewiki_from_json) + # Pass per-provider API keys for mixed provider configurations + configure_codewiki_from_json "$CODEWIKI_CONFIG_JSON" "$CLUSTER_API_KEY" "$MAIN_API_KEY" "$FALLBACK_API_KEY" + + if [ $? -ne 0 ]; then + echo "โŒ CodeWiki configuration failed" + exit 1 + fi + + # Set environment variables for backward compatibility with run-codewiki-analysis.sh + export MAIN_MODEL="$CODEWIKI_GENERATION_MODEL" + export FALLBACK_MODEL_1="$CODEWIKI_FALLBACK_MODEL" + if [ "$PRIMARY_PROVIDER" = "anthropic" ]; then + export ANTHROPIC_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" + else + export OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" + fi + + # Verify configuration was saved + echo "" + echo "๐Ÿ“‹ CodeWiki configuration:" + python -m codewiki config show + + echo "" + echo "โœ… Validating configuration..." + python -m codewiki config validate + + echo "" + set_output "codewiki_configured" "true" + + - name: Run CodeWiki Analysis + id: stage2 + if: contains(env.STAGES, 'codewiki') && steps.detect_language.outputs.codewiki_supported == 'true' && steps.codewiki_config.outputs.codewiki_configured == 'true' + continue-on-error: false + env: + # Keyring backend for CI (must match config step) + PYTHON_KEYRING_BACKEND: keyrings.alt.file.PlaintextKeyring + # API keys for both providers (CodeWiki will use the one configured) + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + # OSS Tenant Structure: Stage 2 outputs + REFERENCE_OUTPUT_PATH: ${{ env.REFERENCE_OUTPUT_PATH }} + DIAGRAMS_OUTPUT_PATH: ${{ env.DIAGRAMS_OUTPUT_PATH }} + # Stage timeout + STAGE2_TIMEOUT_HOURS: ${{ env.STAGE2_TIMEOUT_HOURS }} + # CodeWiki JSON configuration (required for unified function) + CODEWIKI_CONFIG_JSON: ${{ env.CODEWIKI_CONFIG_JSON }} + # CodeWiki model configuration (cluster, generation, fallback) + CODEWIKI_CLUSTER_PROVIDER: ${{ env.CODEWIKI_CLUSTER_PROVIDER }} + CODEWIKI_CLUSTER_MODEL: ${{ env.CODEWIKI_CLUSTER_MODEL }} + CODEWIKI_CLUSTER_MAX_TOKENS: ${{ env.CODEWIKI_CLUSTER_MAX_TOKENS }} + CODEWIKI_CLUSTER_MAX_TOKEN_FIELD: ${{ env.CODEWIKI_CLUSTER_MAX_TOKEN_FIELD }} + CODEWIKI_GENERATION_PROVIDER: ${{ env.CODEWIKI_GENERATION_PROVIDER }} + CODEWIKI_GENERATION_MODEL: ${{ env.CODEWIKI_GENERATION_MODEL }} + CODEWIKI_GENERATION_MAX_TOKENS: ${{ env.CODEWIKI_GENERATION_MAX_TOKENS }} + CODEWIKI_GENERATION_MAX_TOKEN_FIELD: ${{ env.CODEWIKI_GENERATION_MAX_TOKEN_FIELD }} + CODEWIKI_FALLBACK_PROVIDER: ${{ env.CODEWIKI_FALLBACK_PROVIDER }} + CODEWIKI_FALLBACK_MODEL: ${{ env.CODEWIKI_FALLBACK_MODEL }} + CODEWIKI_FALLBACK_MAX_TOKENS: ${{ env.CODEWIKI_FALLBACK_MAX_TOKENS }} + CODEWIKI_FALLBACK_MAX_TOKEN_FIELD: ${{ env.CODEWIKI_FALLBACK_MAX_TOKEN_FIELD }} + CODEWIKI_MAX_DEPTH: ${{ env.CODEWIKI_MAX_DEPTH }} + # Flamingo Markdown Guidelines path for CodeWiki prompts + FLAMINGO_MARKDOWN_GUIDELINES_PATH: /tmp/flamingo-markdown-guidelines.md + # Markdown Validation Rules (injected into all prompts) + VALIDATION_RULES_PATH: ${{ env.VALIDATION_RULES_PATH }} + # Custom AI Instructions (All Stages) + CUSTOM_REPO_INSTRUCTIONS: ${{ env.CUSTOM_INSTRUCTIONS }} + # External Repositories + EXTERNAL_REPOS: ${{ env.EXTERNAL_REPOS }} + # Dependencies (for CodeWiki multi-path support) + DEPENDENCIES: ${{ env.DEPENDENCIES }} + run: | + # Determine per-provider API keys (same logic as Configure step) + if [ "$CODEWIKI_CLUSTER_PROVIDER" = "anthropic" ]; then + export CLUSTER_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" + else + export CLUSTER_API_KEY="${{ secrets.OPENAI_API_KEY }}" + fi + + if [ "$CODEWIKI_GENERATION_PROVIDER" = "anthropic" ]; then + export MAIN_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" + else + export MAIN_API_KEY="${{ secrets.OPENAI_API_KEY }}" + fi + + if [ "$CODEWIKI_FALLBACK_PROVIDER" = "anthropic" ]; then + export FALLBACK_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" + else + export FALLBACK_API_KEY="${{ secrets.OPENAI_API_KEY }}" + fi + + # Verify dependencies directory before CodeWiki runs + echo "" + echo "๐Ÿ” Pre-CodeWiki Dependency Verification:" + echo " Current directory: $(pwd)" + echo " Absolute path: $(realpath .)" + echo "" + + if [ -d "./deps" ]; then + echo " โœ… ./deps EXISTS" + echo " Contents: $(ls -1 ./deps 2>/dev/null | wc -l) repositories" + ls -la ./deps 2>/dev/null | head -5 + else + echo " โŒ ./deps NOT FOUND" + fi + + if [ -d "../deps" ]; then + echo " โœ… ../deps EXISTS" + echo " Absolute: $(realpath ../deps)" + echo " Contents: $(ls -1 ../deps 2>/dev/null | wc -l) repositories" + ls -la ../deps 2>/dev/null | head -5 + else + echo " โŒ ../deps NOT FOUND" + fi + + echo " DEPENDENCIES env: ${DEPENDENCIES:-}" + echo "" + + # Run externalized CodeWiki analysis script + /tmp/run-codewiki-analysis.sh + + # Alternative: Claude Architecture Analysis for ALL languages (when CodeWiki is not supported) + - name: Run Claude Architecture Analysis (All Languages) + id: stage2_alt + if: contains(env.STAGES, 'codewiki') && steps.detect_language.outputs.codewiki_supported == 'false' + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + # SSOT โ€” see workflow env CLAUDE_MODEL block + CLAUDE_MODEL: ${{ env.CLAUDE_MODEL }} + PRIMARY_LANGUAGE: ${{ steps.detect_language.outputs.primary_language }} + # OSS Tenant Structure: Stage 2 outputs + REFERENCE_OUTPUT_PATH: ${{ env.REFERENCE_OUTPUT_PATH }} + DIAGRAMS_OUTPUT_PATH: ${{ env.DIAGRAMS_OUTPUT_PATH }} + # Unified file discovery result (single source of truth) + SOURCE_FILES_LIST: ${{ steps.discover_files.outputs.source_files_list }} + SOURCE_FILE_COUNT: ${{ steps.discover_files.outputs.source_file_count }} + # Custom AI Instructions (All Stages) + CUSTOM_REPO_INSTRUCTIONS: ${{ env.CUSTOM_INSTRUCTIONS }} + # External Repositories + EXTERNAL_REPOS: ${{ env.EXTERNAL_REPOS }} + # Dependencies (for consistency with other stages) + DEPENDENCIES: ${{ env.DEPENDENCIES }} + run: | + # Run externalized Claude architecture analysis script + /tmp/run-claude-architecture-analysis.sh + + # ========================================================================= + # RESTORE STAGE 2 DOCS WHEN STAGE 2 PRODUCED NOTHING + # Clean Slate removed the reference/diagrams trees because `codewiki` was in + # STAGES. If neither Stage-2 variant then completed โ€” a repo with no + # discoverable source, an install failure, a skip โ€” the deletion would be the + # only Stage-2 change in the pull request, i.e. a net removal of documentation + # nobody asked to remove. Put it back. + # ========================================================================= + - name: Restore Docs That No Stage Regenerated + if: always() + env: + BRANCH_NAME: ${{ steps.create-pr-branch.outputs.branch_name }} + STAGE1_STATUS: ${{ steps.stage1.outputs.stage1_status }} + STAGE2_STATUS: ${{ steps.stage2.outputs.stage2_status }} + STAGE2_ALT_STATUS: ${{ steps.stage2_alt.outputs.stage2_status }} + run: | + # Clean Slate deleted whatever the configured stages own, and committed that + # deletion. Any owned subtree whose stage then produced nothing must be put + # back โ€” otherwise the pull request is a net REMOVAL of documentation nobody + # asked to remove. Iterates the exact list Clean Slate recorded, so the + # stage -> subtree map has one home. + if [ -z "$CLEAN_TARGETS_RECORD" ]; then + echo "โญ๏ธ Clean Slate removed nothing โ€” no restore needed" + exit 0 + fi + + STAGE2_OK=false + [ "$STAGE2_STATUS" = "completed" ] && STAGE2_OK=true + [ "$STAGE2_ALT_STATUS" = "completed" ] && STAGE2_OK=true + + # CodeWiki leaves multi-GB scratch behind on a failed run; never let it near + # the index. + rm -rf "$REFERENCE_OUTPUT_PATH/temp" "$DIAGRAMS_OUTPUT_PATH/temp" 2>/dev/null || true + + RESTORED=0 + while IFS= read -r target; do + [ -n "$target" ] || continue + # A target is safe to leave deleted only if something regenerated it. + case "$target" in + "$REFERENCE_OUTPUT_PATH"|"$DIAGRAMS_OUTPUT_PATH") + [ "$STAGE2_OK" = true ] && continue ;; + "$GETTING_STARTED_OUTPUT_PATH"|"$DEVELOPMENT_OUTPUT_PATH") + [ "$STAGE3_STATUS" = "completed" ] && continue ;; + "$DOCS_OUTPUT_PATH") + # Full wipe: only fully safe when every stage delivered. + if [ "$STAGE1_STATUS" = "completed" ] && [ "$STAGE2_OK" = true ] && \ + [ "$STAGE3_STATUS" = "completed" ] && [ "$STAGE4_STATUS" = "completed" ]; then + continue + fi ;; + esac + if git checkout "$PRE_CLEAN_SHA" -- "$target" 2>/dev/null; then + echo " โ†ฉ๏ธ Restored $target (no stage regenerated it)" + RESTORED=1 + fi + done <<< "$CLEAN_TARGETS_RECORD" + + # `git checkout -- ` already stages exactly those paths. Deliberately NO + # `git add -A`: at this point the workspace holds npm install output from + # Stage 1 and, on a failed CodeWiki run, its scratch trees. + if [ "$RESTORED" -eq 1 ] && [ -n "$(git diff --cached --name-only)" ]; then + git commit -m "chore(docs): restore documentation no stage regenerated [skip ci]" + git push origin "$BRANCH_NAME" + echo " โœ… Restore committed" + else + echo " โญ๏ธ Nothing to restore" + fi + + # ========================================================================= + # COMMIT STAGE 2 RESULTS (Progressive PR) + # ========================================================================= + - name: Commit and Push Stage 2 Results + if: always() && (steps.stage2.outputs.stage2_status == 'completed' || steps.stage2_alt.outputs.stage2_status == 'completed') + id: commit-stage2 + env: + BRANCH_NAME: ${{ steps.create-pr-branch.outputs.branch_name }} + STAGE2_FILES: ${{ steps.stage2.outputs.stage2_files || steps.stage2_alt.outputs.stage2_files }} + REFERENCE_OUTPUT_PATH: ${{ env.REFERENCE_OUTPUT_PATH }} + DIAGRAMS_OUTPUT_PATH: ${{ env.DIAGRAMS_OUTPUT_PATH }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Committing Stage 2 (Architecture Analysis) results..." + + # Clean up CodeWiki temp files from all output directories + echo "๐Ÿงน Cleaning up temporary files..." + rm -rf "$REFERENCE_OUTPUT_PATH/temp" 2>/dev/null || echo "โš ๏ธ Warning: Could not remove $REFERENCE_OUTPUT_PATH/temp" + rm -rf "$DIAGRAMS_OUTPUT_PATH/temp" 2>/dev/null || echo "โš ๏ธ Warning: Could not remove $DIAGRAMS_OUTPUT_PATH/temp" + + # Verify cleanup + if [ -d "$REFERENCE_OUTPUT_PATH/temp" ]; then + echo "โŒ ERROR: Failed to cleanup temp files in $REFERENCE_OUTPUT_PATH/temp" + ls -la "$REFERENCE_OUTPUT_PATH/temp" + exit 1 + fi + + # Create .gitignore in CodeWiki output directories to prevent temp files from being committed + echo "๐Ÿ“ Creating .gitignore files in output directories..." + for output_dir in "$REFERENCE_OUTPUT_PATH" "$DIAGRAMS_OUTPUT_PATH"; do + if [ -d "$output_dir" ]; then + { + echo "# CodeWiki temp files (dependency graphs can be 7GB+)" + echo "temp/" + echo "dependency_graphs/" + echo "" + echo "# JSON intermediate files (except schema/config)" + echo "*.json" + echo "!*-schema.json" + echo "!*-config.json" + } > "$output_dir/.gitignore" + echo " โœ… Created $output_dir/.gitignore" + fi + done + + # Debug: Show what files exist before staging + echo "" + echo "๐Ÿ” Debug: Files in Stage 2 output directories BEFORE staging:" + echo " Reference directory ($REFERENCE_OUTPUT_PATH):" + if [ -d "$REFERENCE_OUTPUT_PATH" ]; then + find "$REFERENCE_OUTPUT_PATH" -type f \( -name "*.md" -o -name "*.mmd" -o -name ".gitignore" \) | head -20 + FILE_COUNT=$(find "$REFERENCE_OUTPUT_PATH" -type f \( -name "*.md" -o -name "*.mmd" \) | wc -l | tr -d ' ') + echo " Total .md/.mmd files: $FILE_COUNT" + else + echo " โš ๏ธ Directory does not exist!" + fi + echo "" + echo " Diagrams directory ($DIAGRAMS_OUTPUT_PATH):" + if [ -d "$DIAGRAMS_OUTPUT_PATH" ]; then + find "$DIAGRAMS_OUTPUT_PATH" -type f \( -name "*.md" -o -name "*.mmd" -o -name ".gitignore" \) | head -20 + FILE_COUNT=$(find "$DIAGRAMS_OUTPUT_PATH" -type f \( -name "*.md" -o -name "*.mmd" \) | wc -l | tr -d ' ') + echo " Total .md/.mmd files: $FILE_COUNT" + else + echo " โš ๏ธ Directory does not exist!" + fi + echo "" + + # Stage all .md, .mmd, and .gitignore files from Stage 2 output directories + # CRITICAL: Use git add on full paths to preserve nested directory structure + # This ensures Backend/Authentication/JWT/JWT.md keeps its full path in git + echo "๐Ÿ“ Staging Stage 2 files (excluding JSON/temp files)..." + + # Add only .md, .mmd, .gitignore, and allowed JSON files (*-schema.json, *-config.json) + # This excludes CodeWiki intermediate files: module_tree.json, first_module_tree.json, metadata.json + if [ -d "$REFERENCE_OUTPUT_PATH" ]; then + find "$REFERENCE_OUTPUT_PATH" -type f \( \ + -name "*.md" -o -name "*.mmd" -o -name ".gitignore" \ + -o -name "*-schema.json" -o -name "*-config.json" \ + \) -exec git add {} \; 2>/dev/null || true + echo " Added .md/.mmd/.gitignore/*-schema.json/*-config.json from $REFERENCE_OUTPUT_PATH/" + fi + + if [ -d "$DIAGRAMS_OUTPUT_PATH" ]; then + find "$DIAGRAMS_OUTPUT_PATH" -type f \( \ + -name "*.md" -o -name "*.mmd" -o -name ".gitignore" \ + -o -name "*-schema.json" -o -name "*-config.json" \ + \) -exec git add {} \; 2>/dev/null || true + echo " Added .md/.mmd/.gitignore/*-schema.json/*-config.json from $DIAGRAMS_OUTPUT_PATH/" + fi + + # Check if there are changes + STAGED_COUNT=$(git diff --cached --name-only | wc -l) + echo " Staged $STAGED_COUNT files" + + # Debug: Show what was actually staged + if [ "$STAGED_COUNT" -gt 0 ]; then + echo "" + echo "๐Ÿ” Debug: Files STAGED by git:" + git diff --cached --name-only | head -30 + fi + echo "" + + if [ "$STAGED_COUNT" -gt 0 ]; then + # Commit and push + git commit -m "docs: Stage 2 - Architecture analysis ($STAGE2_FILES files) [skip ci]" + git push origin "$BRANCH_NAME" + + echo "โœ… Committed and pushed $STAGED_COUNT files from Stage 2" + set_output "committed" "true" + else + echo "โŒ ERROR: No Stage 2 files staged for commit!" + echo " Expected $STAGE2_FILES files but staged 0" + echo " This indicates files were generated but not properly staged" + set_output "committed" "false" + fi + + - name: Update PR with Stage 2 Progress + if: always() && steps.commit-stage2.outputs.committed == 'true' && steps.create-initial-pr.outputs.pull-request-number + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.create-initial-pr.outputs.pull-request-number }} + STAGE1_FILES: ${{ steps.stage1.outputs.stage1_files || 0 }} + STAGE2_FILES: ${{ steps.stage2.outputs.stage2_files || steps.stage2_alt.outputs.stage2_files || 0 }} + RUN_ID_VAR: ${{ env.RUN_ID }} + REPO_NAME: ${{ github.repository }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Updating PR description with Stage 2 results..." + + # Update PR body + PR_BODY="## ๐Ÿฆฉ Flamingo AI Technical Writer: In Progress + + **Run ID:** \`$RUN_ID_VAR\` + **Status:** ๐Ÿ”„ Running... + + This PR is being updated as each documentation stage completes. + + ### Progress + - โœ… Stage 1: Inline Documentation - Completed ($STAGE1_FILES files) + - โœ… Stage 2: Architecture Analysis - Completed ($STAGE2_FILES files) + - โณ Stage 3: Tutorial Generation - Running... + - โฑ๏ธ Stage 4: Repository Documentation - Pending + + --- + ๐Ÿฆฉ Generated by [Flamingo AI Technical Writer](https://flamingo.run)" + + gh pr edit "$PR_NUMBER" --body "$PR_BODY" + + echo "โœ… PR #$PR_NUMBER updated with Stage 2 progress" + + - name: Report Stage 2 Progress + if: always() && contains(env.STAGES, 'codewiki') && env.HUB_BASE_URL != '' + continue-on-error: true + env: + WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} + WORKFLOW_RUN_ID: ${{ github.run_id }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + STAGE1_STATUS: ${{ steps.stage1.outputs.stage1_status || 'skipped' }} + STAGE1_FILES: ${{ steps.stage1.outputs.stage1_files || 0 }} + # Use outputs from either CodeWiki (stage2) or Claude alternative (stage2_alt) + STAGE2_STATUS: ${{ steps.stage2.outputs.stage2_status || steps.stage2_alt.outputs.stage2_status || 'skipped' }} + STAGE2_FILES: ${{ steps.stage2.outputs.stage2_files || steps.stage2_alt.outputs.stage2_files || 0 }} + PR_URL: ${{ steps.create-initial-pr.outputs.pull-request-url }} + PR_NUMBER: ${{ steps.create-initial-pr.outputs.pull-request-number }} + run: | + source /tmp/workflow-helpers.sh + + # Construct callback URL from hub base URL + CALLBACK_URL="${HUB_BASE_URL}/api/doc-orchestrator/webhook" + echo "๐Ÿ“ค Reporting Stage 2 (Architecture Analysis) completion with PR URL..." + report_stage_progress "$CALLBACK_URL" "$WEBHOOK_SECRET" "$RUN_ID" "$REPO_ID" \ + "$WORKFLOW_RUN_ID" "$WORKFLOW_URL" "tutorials" \ + "$STAGE1_STATUS" "$STAGE1_FILES" "$STAGE2_STATUS" "$STAGE2_FILES" "" "0" "" "0" \ + "$PR_URL" "$PR_NUMBER" + + # ========================================================================= + # STAGE 3: AI TUTORIAL GENERATOR (VoltAgent-powered) + # Generate getting started guides and how-to tutorials + # Uses VoltAgent framework with tool-based document generation + # Generates 4 tutorials: user/getting-started, user/common-use-cases, + # dev/getting-started-dev, dev/architecture-overview-dev + # ========================================================================= + - name: Install Tutorial Generator Dependencies + if: contains(env.STAGES, 'tutorials') + # Isolated deps tree (see Stage 1) - no reconciliation with the target repo, so no + # peer-conflict band-aids. @ai-sdk/anthropic pinned to 2.x (v2 spec): 3.x uses the + # v3 spec, incompatible with VoltAgent's ai@5.x (AI SDK 5). + run: | + mkdir -p "$RUNNER_TEMP/doc-orch-deps" && cd "$RUNNER_TEMP/doc-orch-deps" + npm init -y >/dev/null 2>&1 + npm install @voltagent/core@2.9.0 "@ai-sdk/anthropic@2.0.91" zod@3.25.76 glob@13.0.6 + + - name: Generate Tutorials with VoltAgent + id: stage3 + if: contains(env.STAGES, 'tutorials') + env: + # SECURITY: Pass secrets per-step with inline masking + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} + # Pass through output paths from workflow env (OSS Tenant Structure) + DOCS_OUTPUT_PATH: ${{ env.DOCS_OUTPUT_PATH }} + # Stage 2 outputs (for context) + REFERENCE_OUTPUT_PATH: ${{ env.REFERENCE_OUTPUT_PATH }} + DIAGRAMS_OUTPUT_PATH: ${{ env.DIAGRAMS_OUTPUT_PATH }} + # Stage 3 outputs + GETTING_STARTED_OUTPUT_PATH: ${{ env.GETTING_STARTED_OUTPUT_PATH }} + DEVELOPMENT_OUTPUT_PATH: ${{ env.DEVELOPMENT_OUTPUT_PATH }} + # Claude model SSOT โ€” see workflow env CLAUDE_MODEL block + CLAUDE_MODEL: ${{ env.CLAUDE_MODEL }} + # Stage timeout + STAGE3_TIMEOUT_HOURS: ${{ env.STAGE3_TIMEOUT_HOURS }} + # Unified file discovery result (same files as Stage 1 and 2) + SOURCE_FILES_LIST: ${{ steps.discover_files.outputs.source_files_list }} + # NODE_PATH to find modules from /tmp/ scripts + NODE_PATH: ${{ runner.temp }}/doc-orch-deps/node_modules + # YouTube Integration - SECURITY: API key from secrets, NOT dispatch payload + YOUTUBE_ENABLED: ${{ env.YOUTUBE_ENABLED }} + YOUTUBE_CHANNELS: ${{ env.YOUTUBE_CHANNELS }} + YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }} + # Markdown Validation Rules (injected into prompts) + VALIDATION_RULES_PATH: ${{ env.VALIDATION_RULES_PATH }} + # Flamingo Markdown Guidelines (optional) + GUIDELINES_PATH: ${{ env.GUIDELINES_PATH }} + # Stage 3 tracking files (configurable paths) + STAGE3_FILES_TRACKER: ${{ env.STAGE3_FILES_TRACKER }} + STAGE3_STATS_FILE: ${{ env.STAGE3_STATS_FILE }} + # Custom AI Instructions (All Stages) + CUSTOM_REPO_INSTRUCTIONS: ${{ env.CUSTOM_INSTRUCTIONS }} + # Analysis Exclusions + EXCLUDED_PATHS: ${{ env.EXCLUDED_PATHS }} + # External Repositories + EXTERNAL_REPOS: ${{ env.EXTERNAL_REPOS }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿค– Stage 3: VoltAgent Tutorial Generator starting..." + + # Run with unified timeout helper (6 hours default) + # Script already downloaded to /tmp/ in setup step + run_with_timeout "Stage 3" "$STAGE3_TIMEOUT_HOURS" node /tmp/generate-tutorials-voltagent.cjs || true + # Continue - don't fail the workflow, preserve partial results + + # Count files from both OSS Tenant Structure directories + GETTING_STARTED_FILES=$(count_markdown_files "${GETTING_STARTED_OUTPUT_PATH}") + DEVELOPMENT_FILES=$(count_markdown_files "${DEVELOPMENT_OUTPUT_PATH}") + TUTORIAL_FILES=$((GETTING_STARTED_FILES + DEVELOPMENT_FILES)) + echo " Getting Started: $GETTING_STARTED_FILES files" + echo " Development: $DEVELOPMENT_FILES files" + echo " Total Stage 3: $TUTORIAL_FILES files" + set_output "stage3_files" "$TUTORIAL_FILES" + set_output "stage3_status" "completed" + + # ========================================================================= + # COMMIT STAGE 3 RESULTS (Progressive PR) + # ========================================================================= + - name: Commit and Push Stage 3 Results + if: always() && steps.stage3.outputs.stage3_status == 'completed' + id: commit-stage3 + env: + BRANCH_NAME: ${{ steps.create-pr-branch.outputs.branch_name }} + STAGE3_FILES: ${{ steps.stage3.outputs.stage3_files }} + GETTING_STARTED_OUTPUT_PATH: ${{ env.GETTING_STARTED_OUTPUT_PATH }} + DEVELOPMENT_OUTPUT_PATH: ${{ env.DEVELOPMENT_OUTPUT_PATH }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Committing Stage 3 (Tutorial Generation) results..." + + # Create .gitignore in VoltAgent output directories + echo "๐Ÿ“ Creating .gitignore files in output directories..." + for output_dir in "$GETTING_STARTED_OUTPUT_PATH" "$DEVELOPMENT_OUTPUT_PATH"; do + if [ -d "$output_dir" ]; then + { + echo "# VoltAgent temp files" + echo "temp/" + echo "" + echo "# JSON intermediate files (except schema/config)" + echo "*.json" + echo "!*-schema.json" + echo "!*-config.json" + } > "$output_dir/.gitignore" + echo " โœ… Created $output_dir/.gitignore" + fi + done + + # Stage all .md and .gitignore files from Stage 3 output directories + find "$GETTING_STARTED_OUTPUT_PATH" "$DEVELOPMENT_OUTPUT_PATH" -type f \( -name "*.md" -o -name ".gitignore" \) \ + -exec git add -f {} \; 2>/dev/null || true + + # Check if there are changes + STAGED_COUNT=$(git diff --cached --name-only | wc -l) + + if [ "$STAGED_COUNT" -gt 0 ]; then + # Commit and push + git commit -m "docs: Stage 3 - Tutorial generation ($STAGE3_FILES files) [skip ci]" + git push origin "$BRANCH_NAME" + + echo "โœ… Committed and pushed $STAGED_COUNT files from Stage 3" + set_output "committed" "true" + else + echo "โš ๏ธ No Stage 3 files to commit" + set_output "committed" "false" + fi + + - name: Update PR with Stage 3 Progress + if: always() && steps.commit-stage3.outputs.committed == 'true' && steps.create-initial-pr.outputs.pull-request-number + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.create-initial-pr.outputs.pull-request-number }} + STAGE1_FILES: ${{ steps.stage1.outputs.stage1_files || 0 }} + STAGE2_FILES: ${{ steps.stage2.outputs.stage2_files || steps.stage2_alt.outputs.stage2_files || 0 }} + STAGE3_FILES: ${{ steps.stage3.outputs.stage3_files || 0 }} + RUN_ID_VAR: ${{ env.RUN_ID }} + REPO_NAME: ${{ github.repository }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Updating PR description with Stage 3 results..." + + # Update PR body + PR_BODY="## ๐Ÿฆฉ Flamingo AI Technical Writer: In Progress + + **Run ID:** \`$RUN_ID_VAR\` + **Status:** ๐Ÿ”„ Running... + + This PR is being updated as each documentation stage completes. + + ### Progress + - โœ… Stage 1: Inline Documentation - Completed ($STAGE1_FILES files) + - โœ… Stage 2: Architecture Analysis - Completed ($STAGE2_FILES files) + - โœ… Stage 3: Tutorial Generation - Completed ($STAGE3_FILES files) + - โณ Stage 4: Repository Documentation - Running... + + --- + ๐Ÿฆฉ Generated by [Flamingo AI Technical Writer](https://flamingo.run)" + + gh pr edit "$PR_NUMBER" --body "$PR_BODY" + + echo "โœ… PR #$PR_NUMBER updated with Stage 3 progress" + + - name: Report Stage 3 Progress + if: always() && contains(env.STAGES, 'tutorials') && env.HUB_BASE_URL != '' + continue-on-error: true + env: + WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} + WORKFLOW_RUN_ID: ${{ github.run_id }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + STAGE1_STATUS: ${{ steps.stage1.outputs.stage1_status || 'skipped' }} + STAGE1_FILES: ${{ steps.stage1.outputs.stage1_files || 0 }} + STAGE2_STATUS: ${{ steps.stage2.outputs.stage2_status || steps.stage2_alt.outputs.stage2_status || 'skipped' }} + STAGE2_FILES: ${{ steps.stage2.outputs.stage2_files || steps.stage2_alt.outputs.stage2_files || 0 }} + STAGE3_STATUS: ${{ steps.stage3.outputs.stage3_status || 'skipped' }} + STAGE3_FILES: ${{ steps.stage3.outputs.stage3_files || 0 }} + PR_URL: ${{ steps.create-initial-pr.outputs.pull-request-url }} + PR_NUMBER: ${{ steps.create-initial-pr.outputs.pull-request-number }} + run: | + source /tmp/workflow-helpers.sh + + # Construct callback URL from hub base URL + CALLBACK_URL="${HUB_BASE_URL}/api/doc-orchestrator/webhook" + echo "๐Ÿ“ค Reporting Stage 3 (AI Tutorial Generator) completion with PR URL..." + report_stage_progress "$CALLBACK_URL" "$WEBHOOK_SECRET" "$RUN_ID" "$REPO_ID" \ + "$WORKFLOW_RUN_ID" "$WORKFLOW_URL" "repo-docs" \ + "$STAGE1_STATUS" "$STAGE1_FILES" "$STAGE2_STATUS" "$STAGE2_FILES" "$STAGE3_STATUS" "$STAGE3_FILES" "" "0" \ + "$PR_URL" "$PR_NUMBER" + + # ========================================================================= + # STAGE 4: REPOSITORY DOCUMENTATION + # Copies LICENSE.md, SECURITY.md from template repo + # Generates/updates README.md, CONTRIBUTING.md using VoltAgent + # ========================================================================= + - name: Generate Repository Documentation + id: stage4 + if: contains(env.STAGES, 'repo-docs') + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + TEMPLATE_REPO: ${{ env.TEMPLATE_REPO }} + TEMPLATE_BRANCH: ${{ env.TEMPLATE_BRANCH }} + DOCS_OUTPUT_PATH: ${{ env.DOCS_OUTPUT_PATH }} + # OSS Tenant Structure: All output paths for docs/README.md navigation + REFERENCE_OUTPUT_PATH: ${{ env.REFERENCE_OUTPUT_PATH }} + DIAGRAMS_OUTPUT_PATH: ${{ env.DIAGRAMS_OUTPUT_PATH }} + GETTING_STARTED_OUTPUT_PATH: ${{ env.GETTING_STARTED_OUTPUT_PATH }} + DEVELOPMENT_OUTPUT_PATH: ${{ env.DEVELOPMENT_OUTPUT_PATH }} + # Claude model SSOT โ€” see workflow env CLAUDE_MODEL block + CLAUDE_MODEL: ${{ env.CLAUDE_MODEL }} + STAGE4_TIMEOUT_HOURS: ${{ env.STAGE4_TIMEOUT_HOURS }} + # NODE_PATH to find modules from /tmp/ scripts + NODE_PATH: ${{ runner.temp }}/doc-orch-deps/node_modules + # YouTube Integration - SECURITY: API key from secrets, NOT dispatch payload + YOUTUBE_ENABLED: ${{ env.YOUTUBE_ENABLED }} + YOUTUBE_CHANNELS: ${{ env.YOUTUBE_CHANNELS }} + YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }} + # Markdown Validation Rules (injected into prompts) + VALIDATION_RULES_PATH: ${{ env.VALIDATION_RULES_PATH }} + # Flamingo Markdown Guidelines (optional) + GUIDELINES_PATH: ${{ env.GUIDELINES_PATH }} + # Stage 4 tracking files (configurable paths) + STAGE4_FILES_TRACKER: ${{ env.STAGE4_FILES_TRACKER }} + # Custom AI Instructions (All Stages) + CUSTOM_REPO_INSTRUCTIONS: ${{ env.CUSTOM_INSTRUCTIONS }} + # Analysis Exclusions + EXCLUDED_PATHS: ${{ env.EXCLUDED_PATHS }} + # External Repositories + EXTERNAL_REPOS: ${{ env.EXTERNAL_REPOS }} + # README Branding + README_LOGO_DARK: ${{ env.README_LOGO_DARK }} + README_LOGO_LIGHT: ${{ env.README_LOGO_LIGHT }} + README_LOGO_ALT: ${{ env.README_LOGO_ALT }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“„ Stage 4: Repository Documentation..." + echo " Template: $TEMPLATE_REPO (branch: $TEMPLATE_BRANCH)" + + RAW_URL="https://raw.githubusercontent.com/$TEMPLATE_REPO/$TEMPLATE_BRANCH" + + # === STEP 1: Copy LICENSE.md and SECURITY.md from template repo === + echo "" + echo "๐Ÿ“ฅ Fetching LICENSE.md from template repo..." + if curl -fsSL "$RAW_URL/LICENSE.md" -o LICENSE.md 2>/dev/null; then + echo " โœ… LICENSE.md copied" + else + echo " โš ๏ธ LICENSE.md not found in template repo (non-blocking)" + fi + + echo "๐Ÿ“ฅ Fetching SECURITY.md from template repo..." + if curl -fsSL "$RAW_URL/SECURITY.md" -o SECURITY.md 2>/dev/null; then + echo " โœ… SECURITY.md copied" + else + echo " โš ๏ธ SECURITY.md not found in template repo (non-blocking)" + fi + + # === STEP 2: Check existing README === + echo "" + if [ -f "README.md" ]; then + README_SIZE=$(wc -c < README.md | tr -d ' ') + echo "๐Ÿ“ Found existing README.md ($README_SIZE bytes) - will use as context" + else + echo "๐Ÿ“ No README.md found" + fi + echo " Generating fresh README with OpenFrame branding..." + + # === STEP 3: Run VoltAgent script (already downloaded in setup step) === + echo "" + echo "๐Ÿค– Generating repository documentation with VoltAgent..." + # Script already downloaded to /tmp/ in Download Workflow Scripts step + run_with_timeout "Stage 4" "$STAGE4_TIMEOUT_HOURS" node /tmp/generate-repo-docs.cjs || true + + # === STEP 4: Count results === + echo "" + echo "๐Ÿ“Š Stage 4 Results:" + REPO_DOCS=0 + for f in README.md CONTRIBUTING.md LICENSE.md SECURITY.md; do + if [ -f "$f" ]; then + SIZE=$(wc -c < "$f" | tr -d ' ') + echo " โœ… $f ($SIZE bytes)" + REPO_DOCS=$((REPO_DOCS + 1)) + fi + done + + set_output "stage4_files" "$REPO_DOCS" + if [ "$REPO_DOCS" -gt 0 ]; then + set_output "stage4_status" "completed" + echo "" + echo "โœ… Stage 4 completed: $REPO_DOCS repository documentation files" + else + set_output "stage4_status" "skipped" + echo "" + echo "โš ๏ธ Stage 4 skipped: No repository documentation files generated" + fi + + # ========================================================================= + # COMMIT STAGE 4 RESULTS (Progressive PR) + # ========================================================================= + - name: Commit and Push Stage 4 Results + if: always() && steps.stage4.outputs.stage4_status == 'completed' + id: commit-stage4 + env: + BRANCH_NAME: ${{ steps.create-pr-branch.outputs.branch_name }} + STAGE4_FILES: ${{ steps.stage4.outputs.stage4_files }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Committing Stage 4 (Repository Documentation) results..." + + # Create .gitignore in Stage 4 managed directories + echo "๐Ÿ“ Creating .gitignore files in managed directories..." + for managed_dir in docs/api docs/deployment docs/operations docs/cli; do + if [ -d "$managed_dir" ]; then + { + echo "# VoltAgent temp files" + echo "temp/" + echo "" + echo "# JSON intermediate files (except schema/config)" + echo "*.json" + echo "!*-schema.json" + echo "!*-config.json" + } > "$managed_dir/.gitignore" + echo " โœ… Created $managed_dir/.gitignore" + fi + done + + # Stage repository documentation files + for f in README.md CONTRIBUTING.md LICENSE.md SECURITY.md; do + if [ -f "$f" ]; then + git add -f "$f" + fi + done + + # Stage Stage 4 managed directories + for managed_dir in docs/api docs/deployment docs/operations docs/cli; do + if [ -d "$managed_dir" ]; then + git add -f "$managed_dir/" 2>/dev/null || true + fi + done + + # Stage docs/README.md if exists + if [ -f "docs/README.md" ]; then + git add -f "docs/README.md" + fi + + # Check if there are changes + STAGED_COUNT=$(git diff --cached --name-only | wc -l) + + if [ "$STAGED_COUNT" -gt 0 ]; then + # Commit and push + git commit -m "docs: Stage 4 - Repository documentation ($STAGE4_FILES files) [skip ci]" + git push origin "$BRANCH_NAME" + + echo "โœ… Committed and pushed $STAGED_COUNT files from Stage 4" + set_output "committed" "true" + else + echo "โš ๏ธ No Stage 4 files to commit" + set_output "committed" "false" + fi + + - name: Update PR with Stage 4 Progress + if: always() && steps.commit-stage4.outputs.committed == 'true' && steps.create-initial-pr.outputs.pull-request-number + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.create-initial-pr.outputs.pull-request-number }} + STAGE1_FILES: ${{ steps.stage1.outputs.stage1_files || 0 }} + STAGE2_FILES: ${{ steps.stage2.outputs.stage2_files || steps.stage2_alt.outputs.stage2_files || 0 }} + STAGE3_FILES: ${{ steps.stage3.outputs.stage3_files || 0 }} + STAGE4_FILES: ${{ steps.stage4.outputs.stage4_files || 0 }} + RUN_ID_VAR: ${{ env.RUN_ID }} + REPO_NAME: ${{ github.repository }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Updating PR description with Stage 4 results..." + + # Update PR body + PR_BODY="## ๐Ÿฆฉ Flamingo AI Technical Writer: In Progress + + **Run ID:** \`$RUN_ID_VAR\` + **Status:** ๐Ÿ”„ Running... + + This PR is being updated as each documentation stage completes. + + ### Progress + - โœ… Stage 1: Inline Documentation - Completed ($STAGE1_FILES files) + - โœ… Stage 2: Architecture Analysis - Completed ($STAGE2_FILES files) + - โœ… Stage 3: Tutorial Generation - Completed ($STAGE3_FILES files) + - โœ… Stage 4: Repository Documentation - Completed ($STAGE4_FILES files) + + --- + ๐Ÿฆฉ Generated by [Flamingo AI Technical Writer](https://flamingo.run)" + + gh pr edit "$PR_NUMBER" --body "$PR_BODY" + + echo "โœ… PR #$PR_NUMBER updated with Stage 4 progress" + + # ========================================================================= + # VALIDATE GENERATED MARKDOWN + # Warn-only validation (never blocks PRs) + # ========================================================================= + - name: Validate Generated Markdown + if: always() + continue-on-error: true # NEVER block PR - validation is warn-only + env: + DOCS_OUTPUT_DIR: ${{ env.DOCS_OUTPUT_PATH }} + # OSS Tenant Structure paths for validation + REFERENCE_OUTPUT_PATH: ${{ env.REFERENCE_OUTPUT_PATH }} + DIAGRAMS_OUTPUT_PATH: ${{ env.DIAGRAMS_OUTPUT_PATH }} + GETTING_STARTED_OUTPUT_PATH: ${{ env.GETTING_STARTED_OUTPUT_PATH }} + DEVELOPMENT_OUTPUT_PATH: ${{ env.DEVELOPMENT_OUTPUT_PATH }} + NODE_PATH: ${{ runner.temp }}/doc-orch-deps/node_modules + run: | + echo "๐Ÿ“‹ Validating generated markdown against Flamingo guidelines..." + node /tmp/validate-markdown.js "$DOCS_OUTPUT_DIR" 2>&1 || true + # Validate OSS Tenant Structure outputs + node /tmp/validate-markdown.js "$REFERENCE_OUTPUT_PATH" 2>&1 || true + node /tmp/validate-markdown.js "$GETTING_STARTED_OUTPUT_PATH" 2>&1 || true + node /tmp/validate-markdown.js "$DEVELOPMENT_OUTPUT_PATH" 2>&1 || true + echo "โœ… Validation complete (warnings logged above, non-blocking)" + + # Report Stage 4 Progress + - name: Report Stage 4 Progress + if: always() && env.HUB_BASE_URL != '' + continue-on-error: true + env: + WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} + WORKFLOW_RUN_ID: ${{ github.run_id }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + STAGE1_STATUS: ${{ steps.stage1.outputs.stage1_status || 'skipped' }} + STAGE1_FILES: ${{ steps.stage1.outputs.stage1_files || 0 }} + STAGE2_STATUS: ${{ steps.stage2.outputs.stage2_status || steps.stage2_alt.outputs.stage2_status || 'skipped' }} + STAGE2_FILES: ${{ steps.stage2.outputs.stage2_files || steps.stage2_alt.outputs.stage2_files || 0 }} + STAGE3_STATUS: ${{ steps.stage3.outputs.stage3_status || 'skipped' }} + STAGE3_FILES: ${{ steps.stage3.outputs.stage3_files || 0 }} + STAGE4_STATUS: ${{ steps.stage4.outputs.stage4_status || 'skipped' }} + STAGE4_FILES: ${{ steps.stage4.outputs.stage4_files || 0 }} + PR_URL: ${{ steps.create-initial-pr.outputs.pull-request-url }} + PR_NUMBER: ${{ steps.create-initial-pr.outputs.pull-request-number }} + run: | + source /tmp/workflow-helpers.sh + + # Construct callback URL from hub base URL + CALLBACK_URL="${HUB_BASE_URL}/api/doc-orchestrator/webhook" + echo "๐Ÿ“ค Reporting Stage 4 (Repository Documentation) completion with PR URL..." + report_stage_progress "$CALLBACK_URL" "$WEBHOOK_SECRET" "$RUN_ID" "$REPO_ID" \ + "$WORKFLOW_RUN_ID" "$WORKFLOW_URL" "creating-pr" \ + "$STAGE1_STATUS" "$STAGE1_FILES" "$STAGE2_STATUS" "$STAGE2_FILES" "$STAGE3_STATUS" "$STAGE3_FILES" \ + "$STAGE4_STATUS" "$STAGE4_FILES" "$PR_URL" "$PR_NUMBER" + + # ========================================================================= + # CREATE PULL REQUEST + # ========================================================================= + - name: Cleanup Temporary Files + run: | + source /tmp/workflow-helpers.sh + echo "๐Ÿงน Cleaning up temporary files before PR creation..." + + # Remove stats files + cleanup_path ".doc-stage1-stats.json" + cleanup_path ".doc-stage3-stats.json" + + # Remove run status file (created for the initial PR). + # Legacy name kept so branches started before the rename still clean up. + cleanup_path ".flamingo-ai-technical-writer-status.md" + cleanup_path ".doc-pipeline-status.md" + + # Note: .doc-orchestrator-source-files.txt is now in /tmp/ (auto-cleanup) + + # Remove npm artifacts (installed for scripts) + cleanup_path "node_modules" + cleanup_path "package.json" + cleanup_path "package-lock.json" + + # NOTE: /tmp/workflow-helpers.sh is cleaned up in final webhook step + echo "โœ… Cleanup complete" + + - name: Stage Generated Documentation + id: stage-docs + run: | + source /tmp/workflow-helpers.sh + echo "๐Ÿ“ Staging generated documentation files..." + echo " DOCS_OUTPUT_PATH: $DOCS_OUTPUT_PATH" + echo " OSS Tenant Structure paths:" + echo " Stage 2: $REFERENCE_OUTPUT_PATH (reference)" + echo " Stage 2: $DIAGRAMS_OUTPUT_PATH (diagrams)" + echo " Stage 3: $GETTING_STARTED_OUTPUT_PATH (getting-started)" + echo " Stage 3: $DEVELOPMENT_OUTPUT_PATH (development)" + + # Count untracked/modified files before staging + BEFORE_COUNT=$(git status --porcelain | wc -l) + echo " Total changed files: $BEFORE_COUNT" + + # Stage ALL .md and .mmd files anywhere in the repo (for inline docs generated next to source files) + # This catches Stage 1 inline docs (hidden: .FileName.md), Stage 2 reference/diagrams, Stage 3 tutorials, and Stage 4 repo docs + echo " Finding all .md and .mmd files to stage (including hidden and diagrams)..." + # Find all .md and .mmd files recursively, including hidden files (.*.md) + # Includes README.md, CONTRIBUTING.md, LICENSE.md, SECURITY.md from Stage 4 + # Includes .mmd Mermaid diagram files from Stage 2 (CodeWiki/Claude architecture) + find . \( -name "*.md" -o -name ".*.md" -o -name "*.mmd" \) -type f \ + -not -path "./node_modules/*" \ + -not -path "./.git/*" \ + -not -name "CHANGELOG.md" \ + -exec git add -f {} \; 2>/dev/null || true + + # Show what .md and .mmd files exist (for debugging) + echo "" + echo "๐Ÿ“‹ All .md and .mmd files found (including hidden inline docs and diagrams):" + find . \( -name "*.md" -o -name ".*.md" -o -name "*.mmd" \) -type f \ + -not -path "./node_modules/*" \ + -not -path "./.git/*" \ + -not -name "CHANGELOG.md" | head -100 + + # Count staged files + STAGED_COUNT=$(git diff --cached --name-only | wc -l) + echo " Staged files: $STAGED_COUNT" + set_output "staged_count" "$STAGED_COUNT" + + # Show what was staged + echo "" + echo "๐Ÿ“‹ Staged files:" + git diff --cached --name-only | head -50 + + if [ "$STAGED_COUNT" -eq "0" ]; then + echo "" + echo "โš ๏ธ No documentation files to stage" + set_output "has_changes" "false" + else + set_output "has_changes" "true" + fi + + - name: Update Final PR Status + if: steps.create-initial-pr.outputs.pull-request-number + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.create-initial-pr.outputs.pull-request-number }} + STAGE1_STATUS: ${{ steps.stage1.outputs.stage1_status || 'skipped' }} + STAGE1_FILES: ${{ steps.stage1.outputs.stage1_files || 0 }} + STAGE2_STATUS: ${{ steps.stage2.outputs.stage2_status || steps.stage2_alt.outputs.stage2_status || 'skipped' }} + STAGE2_FILES: ${{ steps.stage2.outputs.stage2_files || steps.stage2_alt.outputs.stage2_files || 0 }} + STAGE3_STATUS: ${{ steps.stage3.outputs.stage3_status || 'skipped' }} + STAGE3_FILES: ${{ steps.stage3.outputs.stage3_files || 0 }} + STAGE4_STATUS: ${{ steps.stage4.outputs.stage4_status || 'skipped' }} + STAGE4_FILES: ${{ steps.stage4.outputs.stage4_files || 0 }} + RUN_ID_VAR: ${{ env.RUN_ID }} + REPO_NAME: ${{ github.repository }} + run: | + source /tmp/workflow-helpers.sh + + echo "๐Ÿ“ Updating PR with final status..." + + # Remove "in-progress" label + gh pr edit "$PR_NUMBER" --remove-label "in-progress" || true + + # Update title to remove [IN PROGRESS] + gh pr edit "$PR_NUMBER" --title "๐Ÿฆฉ Flamingo AI Technical Writer" + + # Update body with final results + PR_BODY="## ๐Ÿฆฉ Flamingo AI Technical Writer: Complete + + **Run ID:** \`$RUN_ID_VAR\` + + ### Stage 1: Inline Documentation + - Status: $STAGE1_STATUS + - Files generated: $STAGE1_FILES + - Generated .md files next to source classes explaining their purpose + + ### Stage 2: Architecture Analysis + - Status: $STAGE2_STATUS + - Files generated: $STAGE2_FILES + - Architecture overview and module documentation + + ### Stage 3: AI Tutorial Generator + - Status: $STAGE3_STATUS + - Files generated: $STAGE3_FILES + - Getting started guides and how-to tutorials + + ### Stage 4: Repository Documentation + - Status: $STAGE4_STATUS + - Files generated: $STAGE4_FILES + - README.md, CONTRIBUTING.md, LICENSE.md, SECURITY.md + + --- + + **Review checklist:** + - [ ] Check generated inline docs for accuracy + - [ ] Review architecture documentation + - [ ] Test code examples in tutorials + - [ ] Review README.md and CONTRIBUTING.md updates + + --- + ๐Ÿฆฉ Generated by [Flamingo AI Technical Writer](https://flamingo.run)" + + gh pr edit "$PR_NUMBER" --body "$PR_BODY" + + echo "โœ… PR #$PR_NUMBER updated with final status" + + # ========================================================================= + # SEND WEBHOOK NOTIFICATION + # ========================================================================= + - name: Send Webhook Notification + if: always() && env.HUB_BASE_URL != '' + continue-on-error: true # Don't fail the workflow if callback fails + env: + # SECURITY: Pass secret per-step with inline masking + WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} + WORKFLOW_RUN_ID: ${{ github.run_id }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + HAS_CHANGES: ${{ steps.stage-docs.outputs.has_changes }} + JOB_STATUS: ${{ job.status }} + PR_URL: ${{ steps.create-initial-pr.outputs.pull-request-url }} + PR_NUMBER: ${{ steps.create-initial-pr.outputs.pull-request-number || 'null' }} + SAFE_RUN_ID: ${{ steps.branch-name-early.outputs.safe_run_id }} + # Single source of truth for the branch name (was rebuilt by hand below, + # which silently drifted from "Create PR Branch" on every rename) + BRANCH_NAME: ${{ steps.create-pr-branch.outputs.branch_name }} + STAGE1_STATUS: ${{ steps.stage1.outputs.stage1_status || 'skipped' }} + STAGE1_FILES: ${{ steps.stage1.outputs.stage1_files || 0 }} + # Stage 2: Check both CodeWiki and Claude alternative, mark as failed if step failed + STAGE2_STATUS: ${{ steps.stage2.outcome == 'failure' && 'failed' || steps.stage2_alt.outcome == 'failure' && 'failed' || steps.stage2.outputs.stage2_status || steps.stage2_alt.outputs.stage2_status || 'skipped' }} + STAGE2_FILES: ${{ steps.stage2.outputs.stage2_files || steps.stage2_alt.outputs.stage2_files || 0 }} + STAGE3_STATUS: ${{ steps.stage3.outputs.stage3_status || 'skipped' }} + STAGE3_FILES: ${{ steps.stage3.outputs.stage3_files || 0 }} + STAGE4_STATUS: ${{ steps.stage4.outputs.stage4_status || 'skipped' }} + STAGE4_FILES: ${{ steps.stage4.outputs.stage4_files || 0 }} + # Track if critical steps failed (continue-on-error: false steps) + STAGE2_OUTCOME: ${{ steps.stage2.outcome || 'skipped' }} + CODEWIKI_INSTALL_OUTCOME: ${{ steps.codewiki_install.outcome || 'skipped' }} + CODEWIKI_CONFIG_OUTCOME: ${{ steps.codewiki_config.outcome || 'skipped' }} + run: | + source /tmp/workflow-helpers.sh + + # Construct callback URL from hub base URL + CALLBACK_URL="${HUB_BASE_URL}/api/doc-orchestrator/webhook" + + echo "๐Ÿ“Š Determining final workflow status..." + echo " JOB_STATUS: $JOB_STATUS" + echo " HAS_CHANGES: $HAS_CHANGES" + echo " STAGE2_OUTCOME: $STAGE2_OUTCOME" + echo " CODEWIKI_INSTALL_OUTCOME: $CODEWIKI_INSTALL_OUTCOME" + echo " CODEWIKI_CONFIG_OUTCOME: $CODEWIKI_CONFIG_OUTCOME" + + # CRITICAL: Determine final status - NEVER return "running" + # Default to failure, only set success if everything checks out + STATUS="failure" + + # Check for cancelled job first + if [ "$JOB_STATUS" = "cancelled" ]; then + STATUS="cancelled" + echo " โŒ Status: cancelled (workflow was cancelled)" + # Check if critical stage 2 (CodeWiki) failed - this has continue-on-error: false + elif [ "$STAGE2_OUTCOME" = "failure" ]; then + STATUS="failure" + echo " โŒ Status: failure (CodeWiki stage failed)" + # Check if CodeWiki installation failed + elif [ "$CODEWIKI_INSTALL_OUTCOME" = "failure" ]; then + STATUS="failure" + echo " โŒ Status: failure (CodeWiki installation failed)" + # Check if CodeWiki configuration failed + elif [ "$CODEWIKI_CONFIG_OUTCOME" = "failure" ]; then + STATUS="failure" + echo " โŒ Status: failure (CodeWiki configuration failed)" + # Check overall job status + elif [ "$JOB_STATUS" != "success" ]; then + STATUS="failure" + echo " โŒ Status: failure (job status: $JOB_STATUS)" + # Check if we have any documentation changes + elif [ "$HAS_CHANGES" != "true" ]; then + STATUS="no_changes" + echo " โš ๏ธ Status: no_changes (no documentation files generated)" + else + STATUS="success" + echo " โœ… Status: success" + fi + + # SAFETY CHECK: Ensure status is NEVER "running" + if [ "$STATUS" = "running" ] || [ -z "$STATUS" ]; then + echo " ๐Ÿšจ SAFETY: Detected invalid status '$STATUS', forcing to 'failure'" + STATUS="failure" + fi + + echo "" + echo "๐Ÿ“ค Final status to report: $STATUS" + + # Branch actually created by "Create PR Branch". Falls back to the same + # formula only when that step never ran (this step is `if: always()`). + SAFE_BRANCH="${BRANCH_NAME:-docs/flamingo-ai-technical-writer-$SAFE_RUN_ID}" + + # Send final webhook using helper function + report_final_status "$CALLBACK_URL" "$WEBHOOK_SECRET" "$RUN_ID" "$REPO_ID" \ + "$WORKFLOW_RUN_ID" "$WORKFLOW_URL" "$STATUS" "$PR_URL" "$PR_NUMBER" "$SAFE_BRANCH" \ + "$STAGE1_STATUS" "$STAGE1_FILES" "$STAGE2_STATUS" "$STAGE2_FILES" "$STAGE3_STATUS" "$STAGE3_FILES" \ + "$STAGE4_STATUS" "$STAGE4_FILES" + + # Final cleanup: remove workflow helpers file + cleanup_path "/tmp/workflow-helpers.sh" + + - name: Pipeline Summary + if: always() + run: | + echo "## ๐Ÿฆฉ Flamingo AI Technical Writer Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Stage | Status | Files |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Inline Docs | ${{ steps.stage1.outputs.stage1_status || 'skipped' }} | ${{ steps.stage1.outputs.stage1_files || '0' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Architecture | ${{ steps.stage2.outputs.stage2_status || steps.stage2_alt.outputs.stage2_status || 'skipped' }} | ${{ steps.stage2.outputs.stage2_files || steps.stage2_alt.outputs.stage2_files || '0' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Tutorials | ${{ steps.stage3.outputs.stage3_status || 'skipped' }} | ${{ steps.stage3.outputs.stage3_files || '0' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Repo Docs | ${{ steps.stage4.outputs.stage4_status || 'skipped' }} | ${{ steps.stage4.outputs.stage4_files || '0' }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ -n "${{ steps.create-initial-pr.outputs.pull-request-url }}" ]; then + echo "**Pull Request:** ${{ steps.create-initial-pr.outputs.pull-request-url }}" >> $GITHUB_STEP_SUMMARY + fi