From 740d0292f0d60c194ddd120c21cdf033a839bcb8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:11:15 +0000 Subject: [PATCH 1/2] Simplify preview-docs workflow: use github-script@v7 and remove visual diffing Co-Authored-By: kenny@buildwithfern.com --- .github/workflows/preview-docs.yml | 176 ++++++++++++----------------- 1 file changed, 73 insertions(+), 103 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index e326885d7..018806650 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -10,26 +10,21 @@ jobs: run: runs-on: ubuntu-latest permissions: - pull-requests: write # Only for commenting - contents: read # For checking out code + pull-requests: write + contents: read steps: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 0 # Fetch full history for git diff + fetch-depth: 0 - name: Checkout PR run: | git fetch origin pull/${{ github.event.pull_request.number }}/head:pr-${{ github.event.pull_request.number }} git checkout pr-${{ github.event.pull_request.number }} - - name: Install Fern and CML - run: | - npm install -g fern-api@latest - npm install -g @dvcorg/cml - - - name: Install Chrome for Puppeteer - run: npx puppeteer browsers install chrome@141.0.7390.54 + - name: Install Fern + run: npm install -g fern-api@latest - name: Generate preview URL id: generate-docs @@ -42,109 +37,84 @@ jobs: echo "preview_url=$URL" >> $GITHUB_OUTPUT echo "Preview URL: $URL" - - name: Run fern docs diff for changed MDX files - id: docs-diff - env: - FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + - name: Get changed MDX files and slugs + id: changed-files run: | - PREVIEW_URL="${{ steps.generate-docs.outputs.preview_url }}" CHANGED_FILES=$(git diff --name-only origin/main...HEAD -- '*.mdx' 2>/dev/null || echo "") - if [ -z "$CHANGED_FILES" ] || [ -z "$PREVIEW_URL" ]; then - echo "has_diffs=false" >> $GITHUB_OUTPUT + if [ -z "$CHANGED_FILES" ]; then + echo "has_changes=false" >> $GITHUB_OUTPUT exit 0 fi - # Convert newlines to space-separated list for the command - FILES_LIST=$(echo "$CHANGED_FILES" | tr '\n' ' ') - - # Create output directory - mkdir -p .fern/diff - - # Run fern docs diff and capture output - # Progress messages go to stderr, JSON goes to stdout - # We capture everything to a temp file, then extract just the JSON - npx fern-api@latest docs diff "$PREVIEW_URL" $FILES_LIST --output .fern/diff > .fern/diff/output.txt 2>&1 || true - - # Extract just the JSON part (starts with { and ends with }) - # The JSON is the last thing output, so we find the line starting with { and take everything from there - sed -n '/^{/,$p' .fern/diff/output.txt > .fern/diff/diff.json + echo "has_changes=true" >> $GITHUB_OUTPUT - # Debug: show what we captured - echo "=== Raw output ===" - cat .fern/diff/output.txt - echo "=== Extracted JSON ===" - cat .fern/diff/diff.json - - # Check if diff JSON file exists and has valid content - if [ -f ".fern/diff/diff.json" ] && grep -q '"diffs"' .fern/diff/diff.json 2>/dev/null; then - echo "has_diffs=true" >> $GITHUB_OUTPUT - else - echo "has_diffs=false" >> $GITHUB_OUTPUT - echo "No diffs found or diff.json is invalid" - fi - - - name: Upload diff images as artifacts - if: steps.docs-diff.outputs.has_diffs == 'true' - uses: actions/upload-artifact@v4 - with: - name: docs-diff-images - path: .fern/diff/*.png - retention-days: 7 - - - name: Upload images and create comment - if: steps.docs-diff.outputs.has_diffs == 'true' - env: - REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PREVIEW_URL: ${{ steps.generate-docs.outputs.preview_url }} - run: | - # Parse diff.json and upload images using cml - PREVIEW_URL="${{ steps.generate-docs.outputs.preview_url }}" - BASE_URL=$(echo "$PREVIEW_URL" | grep -oP 'https?://[^/]+') - - echo ":herb: **Preview your docs:** <${PREVIEW_URL}>" > comment.md - echo "" >> comment.md - echo "### Visual changes detected:" >> comment.md - echo "" >> comment.md - - # Process each diff entry - jq -c '.diffs[]' .fern/diff/diff.json | while read -r diff; do - FILE=$(echo "$diff" | jq -r '.file') - SLUG=$(echo "$diff" | jq -r '.slug') - COMPARISON=$(echo "$diff" | jq -r '.comparison') - CHANGE_PERCENT=$(echo "$diff" | jq -r '.changePercent // "N/A"') - IS_NEW_PAGE=$(echo "$diff" | jq -r '.isNewPage') - - echo "#### \`${FILE}\`" >> comment.md - echo "" >> comment.md - echo "**Page:** [${SLUG}](${BASE_URL}/${SLUG}) | **Change:** ${CHANGE_PERCENT}%" >> comment.md - if [ "$IS_NEW_PAGE" = "true" ]; then - echo " | *New page*" >> comment.md - fi - echo "" >> comment.md - - # Upload image using cml and get URL - if [ -f "$COMPARISON" ]; then - IMAGE_URL=$(cml publish "$COMPARISON" 2>/dev/null || echo "") - if [ -n "$IMAGE_URL" ]; then - echo "![${FILE} comparison](${IMAGE_URL})" >> comment.md - else - echo "*Comparison image available in [workflow artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*" >> comment.md + # Build JSON array of changed files with their slugs + echo "changes<> $GITHUB_OUTPUT + echo "$CHANGED_FILES" | while read -r file; do + if [ -n "$file" ] && [ -f "$file" ]; then + # Try to extract slug from frontmatter + SLUG=$(sed -n '/^---$/,/^---$/p' "$file" | grep -E '^slug:' | sed 's/^slug:[[:space:]]*//' | tr -d '"' | tr -d "'" || echo "") + + if [ -z "$SLUG" ]; then + # Derive slug from file path (remove fern/products prefix, pages folder, and .mdx extension) + SLUG=$(echo "$file" | sed 's|^fern/products/[^/]*/||' | sed 's|^pages/||' | sed 's|\.mdx$||' | sed 's|/index$||') fi + + echo "${file}|${SLUG}" fi - echo "" >> comment.md done - - cat comment.md - - - name: Create comment without diffs - if: steps.docs-diff.outputs.has_diffs != 'true' - run: | - echo ":herb: **Preview your docs:** <${{ steps.generate-docs.outputs.preview_url }}>" > comment.md + echo "EOF" >> $GITHUB_OUTPUT - name: Post PR comment - uses: thollander/actions-comment-pull-request@v2.4.3 + uses: actions/github-script@v7 with: - filePath: comment.md - comment_tag: preview-docs - mode: upsert + script: | + const previewUrl = '${{ steps.generate-docs.outputs.preview_url }}'; + const hasChanges = '${{ steps.changed-files.outputs.has_changes }}' === 'true'; + const changesRaw = `${{ steps.changed-files.outputs.changes }}`; + + let body = `:herb: **Preview your docs:** <${previewUrl}>\n\n`; + + if (hasChanges && changesRaw.trim()) { + const baseUrl = previewUrl ? new URL(previewUrl).origin : ''; + + body += `### Changed pages:\n\n`; + + const lines = changesRaw.trim().split('\n').filter(line => line.trim()); + for (const line of lines) { + const [file, slug] = line.split('|'); + if (file && slug) { + const pageUrl = `${baseUrl}/${slug}`; + body += `- \`${file}\` → [${slug}](${pageUrl})\n`; + } + } + } + + // Find existing comment to update + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes(':herb: **Preview your docs:**') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } From d837c562c6aabe0340ad089d7bd9f5bc7ea8fcfc Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:15:42 +0000 Subject: [PATCH 2/2] Use get-slug-for-file API endpoint instead of custom slug extraction Co-Authored-By: kenny@buildwithfern.com --- .github/workflows/preview-docs.yml | 57 +++++++++++++++--------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 018806650..f85699654 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -39,31 +39,29 @@ jobs: - name: Get changed MDX files and slugs id: changed-files + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} run: | + PREVIEW_URL="${{ steps.generate-docs.outputs.preview_url }}" CHANGED_FILES=$(git diff --name-only origin/main...HEAD -- '*.mdx' 2>/dev/null || echo "") - if [ -z "$CHANGED_FILES" ]; then + if [ -z "$CHANGED_FILES" ] || [ -z "$PREVIEW_URL" ]; then echo "has_changes=false" >> $GITHUB_OUTPUT exit 0 fi echo "has_changes=true" >> $GITHUB_OUTPUT - # Build JSON array of changed files with their slugs - echo "changes<> $GITHUB_OUTPUT - echo "$CHANGED_FILES" | while read -r file; do - if [ -n "$file" ] && [ -f "$file" ]; then - # Try to extract slug from frontmatter - SLUG=$(sed -n '/^---$/,/^---$/p' "$file" | grep -E '^slug:' | sed 's/^slug:[[:space:]]*//' | tr -d '"' | tr -d "'" || echo "") - - if [ -z "$SLUG" ]; then - # Derive slug from file path (remove fern/products prefix, pages folder, and .mdx extension) - SLUG=$(echo "$file" | sed 's|^fern/products/[^/]*/||' | sed 's|^pages/||' | sed 's|\.mdx$||' | sed 's|/index$||') - fi - - echo "${file}|${SLUG}" - fi - done + # Call the API to get slugs for the changed files + FILES_PARAM=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//') + RESPONSE=$(curl -sf -H "FERN_TOKEN: $FERN_TOKEN" "${PREVIEW_URL}/api/fern-docs/get-slug-for-file?files=${FILES_PARAM}" 2>/dev/null) || { + echo "api_response=" >> $GITHUB_OUTPUT + exit 0 + } + + # Store the API response for the next step + echo "api_response<> $GITHUB_OUTPUT + echo "$RESPONSE" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Post PR comment @@ -72,22 +70,25 @@ jobs: script: | const previewUrl = '${{ steps.generate-docs.outputs.preview_url }}'; const hasChanges = '${{ steps.changed-files.outputs.has_changes }}' === 'true'; - const changesRaw = `${{ steps.changed-files.outputs.changes }}`; + const apiResponseRaw = `${{ steps.changed-files.outputs.api_response }}`; let body = `:herb: **Preview your docs:** <${previewUrl}>\n\n`; - if (hasChanges && changesRaw.trim()) { - const baseUrl = previewUrl ? new URL(previewUrl).origin : ''; - - body += `### Changed pages:\n\n`; - - const lines = changesRaw.trim().split('\n').filter(line => line.trim()); - for (const line of lines) { - const [file, slug] = line.split('|'); - if (file && slug) { - const pageUrl = `${baseUrl}/${slug}`; - body += `- \`${file}\` → [${slug}](${pageUrl})\n`; + if (hasChanges && apiResponseRaw.trim()) { + try { + const response = JSON.parse(apiResponseRaw); + const mappings = response.mappings || []; + const validMappings = mappings.filter(m => m.slug != null); + + if (validMappings.length > 0) { + body += `### Changed pages:\n\n`; + for (const mapping of validMappings) { + const pageUrl = `${previewUrl}/${mapping.slug}`; + body += `- \`${mapping.file}\` → [${mapping.slug}](${pageUrl})\n`; + } } + } catch (e) { + console.log('Failed to parse API response:', e); } }