/** * Preview Comment Manager * Creates and updates sticky PR comments for preview status. * * Usage: Called from GitHub Actions via actions/github-script */ const COMMENT_MARKER = ''; /** * Sanitize text for safe display in code blocks * Prevents XSS by escaping code fences and limiting length * @param {string} text - Text to sanitize * @param {number} maxLength - Maximum length (default: 1000) * @returns {string} - Sanitized text */ function sanitizeForCodeBlock(text, maxLength = 1000) { if (!text) return 'Unknown error'; return text.replace(/```/g, '` `` `').substring(0, maxLength); } /** * Generate preview comment body * @param {Object} options - Comment options * @param {string} options.status - Status: 'success' | 'pending' | 'failed' | 'skipped' * @param {string} [options.previewUrl] - Preview URL (for success) * @param {string[]} [options.pages] - Array of page URLs (for success) * @param {string} [options.buildTime] - Build duration string (for success) * @param {string} [options.errorMessage] - Error message (for failed) * @param {string} [options.skipReason] - Skip reason (for skipped) * @param {boolean} [options.needsInput] - Boolean (for pending) * @returns {string} - Markdown comment body */ export function generatePreviewComment(options) { const { status, // 'success' | 'pending' | 'failed' | 'skipped' previewUrl, // Preview URL (for success) pages, // Array of page URLs (for success) buildTime, // Build duration string (for success) errorMessage, // Error message (for failed) skipReason, // Skip reason (for skipped) needsInput, // Boolean (for pending) } = options; const timestamp = new Date().toISOString().replace('T', ' ').split('.')[0] + ' UTC'; let body = `${COMMENT_MARKER}\n## PR Preview\n\n`; switch (status) { case 'success': body += `| Status | Details |\n`; body += `|--------|----------|\n`; body += `| **Preview** | [View preview](${previewUrl}) |\n`; body += `| **Pages** | ${pages.length} page(s) deployed |\n`; if (buildTime) { body += `| **Build time** | ${buildTime} |\n`; } body += `| **Last updated** | ${timestamp} |\n\n`; if (pages.length > 0) { body += `
\nPages included in this preview\n\n`; const displayPages = pages.slice(0, 20); displayPages.forEach(page => { const safePage = page.replace(/`/g, '\\`'); body += `- \`${safePage}\`\n`; }); if (pages.length > 20) { body += `- ... and ${pages.length - 20} more\n`; } body += `\n
\n\n`; } body += `---\nPreview auto-deploys on push. Will be cleaned up when PR closes.`; break; case 'pending': if (needsInput) { body += `### Preview pages needed\n\n`; body += `This PR changes layout/asset files but doesn't specify which pages to preview.\n\n`; body += `**To generate a preview**, add documentation URLs to your PR description, for example:\n`; body += `\`\`\`\nPlease review:\n- https://docs.influxdata.com/influxdb3/core/get-started/\n- /telegraf/v1/plugins/\n\`\`\`\n\n`; body += `Then re-run the workflow or push a new commit.\n\n`; body += `---\nLast checked: ${timestamp}`; } else { body += `⏳ **Preview building...**\n\n`; body += `---\nStarted: ${timestamp}`; } break; case 'failed': body += `### Preview failed\n\n`; body += `The preview build encountered an error:\n\n`; body += `\`\`\`\n${sanitizeForCodeBlock(errorMessage)}\n\`\`\`\n\n`; body += `[View workflow logs](https://github.com/influxdata/docs-v2/actions)\n\n`; body += `---\nFailed: ${timestamp}`; break; case 'skipped': body += `### Preview skipped\n\n`; body += `${sanitizeForCodeBlock(skipReason || 'No previewable changes detected.', 500)}\n\n`; body += `---\nChecked: ${timestamp}`; break; } return body; } /** * Find existing preview comment on PR * @param {Object} github - GitHub API client * @param {Object} context - GitHub Actions context * @returns {Object|null} - Existing comment or null */ export async function findExistingComment(github, context) { const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); return comments.find(comment => comment.body.includes(COMMENT_MARKER)); } /** * Create or update preview comment * @param {Object} github - GitHub API client * @param {Object} context - GitHub Actions context * @param {Object} options - Comment options */ export async function upsertPreviewComment(github, context, options) { const body = generatePreviewComment(options); const existingComment = await findExistingComment(github, context); if (existingComment) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existingComment.id, body, }); console.log(`Updated existing comment: ${existingComment.id}`); } else { const { data: newComment } = await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body, }); console.log(`Created new comment: ${newComment.id}`); } } /** * Delete preview comment (used on PR close) * @param {Object} github - GitHub API client * @param {Object} context - GitHub Actions context */ export async function deletePreviewComment(github, context) { const existingComment = await findExistingComment(github, context); if (existingComment) { await github.rest.issues.deleteComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existingComment.id, }); console.log(`Deleted comment: ${existingComment.id}`); } }