|
| 1 | +# Reusable workflow that bundles project docs and triggers public portal sync |
| 2 | +# - Collects README and docs markdown, adds sync metadata, and uploads a short-lived artifact |
| 3 | +# - Dispatches a repository event so hoverkraft-tech/public-docs can ingest and publish updates |
| 4 | +--- |
| 5 | +name: Push Documentation Helper |
| 6 | + |
| 7 | +on: |
| 8 | + workflow_call: |
| 9 | + inputs: |
| 10 | + docs_path: |
| 11 | + description: "Path to documentation in source repo (default: docs)" |
| 12 | + required: false |
| 13 | + type: string |
| 14 | + default: "docs" |
| 15 | + include_readme: |
| 16 | + description: "Include README.md from repository root" |
| 17 | + required: false |
| 18 | + type: boolean |
| 19 | + default: true |
| 20 | + secrets: |
| 21 | + PUBLIC_DOCS_TOKEN: |
| 22 | + description: "GitHub token with write access to trigger repository_dispatch in public-docs" |
| 23 | + required: true |
| 24 | + |
| 25 | +jobs: |
| 26 | + prepare-and-dispatch: |
| 27 | + runs-on: ubuntu-latest |
| 28 | + permissions: |
| 29 | + contents: read |
| 30 | + |
| 31 | + steps: |
| 32 | + - name: Checkout repository |
| 33 | + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |
| 34 | + |
| 35 | + - name: Prepare documentation bundle |
| 36 | + id: prepare |
| 37 | + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 |
| 38 | + env: |
| 39 | + SOURCE_REPO: ${{ github.repository }} |
| 40 | + INCLUDE_README: ${{ inputs.include_readme }} |
| 41 | + DOCS_PATH: ${{ inputs.docs_path }} |
| 42 | + with: |
| 43 | + script: | |
| 44 | + const fs = require('fs'); |
| 45 | + const path = require('path'); |
| 46 | + const { execSync } = require('child_process'); |
| 47 | +
|
| 48 | + core.info(`📦 Preparing documentation bundle`); |
| 49 | +
|
| 50 | + // Get current branch name (default branch) |
| 51 | + const currentBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); |
| 52 | + core.setOutput('branch', currentBranch); |
| 53 | + core.info(`Branch: ${currentBranch}`); |
| 54 | +
|
| 55 | + // Create artifact directory |
| 56 | + const artifactDir = 'documentation-artifact'; |
| 57 | + fs.mkdirSync(artifactDir, { recursive: true }); |
| 58 | +
|
| 59 | + // Function to add frontmatter |
| 60 | + function addFrontmatter(filePath, sourcePath) { |
| 61 | + const content = fs.readFileSync(filePath, 'utf8'); |
| 62 | + const now = new Date().toISOString(); |
| 63 | + |
| 64 | + let newContent; |
| 65 | + |
| 66 | + // Check if file already has frontmatter |
| 67 | + if (content.startsWith('---\n')) { |
| 68 | + // Extract existing frontmatter |
| 69 | + const endIndex = content.indexOf('\n---\n', 4); |
| 70 | + if (endIndex !== -1) { |
| 71 | + const existingFrontmatter = content.substring(4, endIndex); |
| 72 | + const bodyContent = content.substring(endIndex + 5); |
| 73 | + |
| 74 | + // Add source metadata to existing frontmatter |
| 75 | + newContent = `---\n${existingFrontmatter}\nsource_repo: ${process.env.SOURCE_REPO}\nsource_path: ${sourcePath}\nsource_branch: ${currentBranch}\nlast_synced: ${now}\n---\n${bodyContent}`; |
| 76 | + } else { |
| 77 | + // Malformed frontmatter, treat as no frontmatter |
| 78 | + newContent = `---\nsource_repo: ${process.env.SOURCE_REPO}\nsource_path: ${sourcePath}\nsource_branch: ${currentBranch}\nlast_synced: ${now}\n---\n\n${content}`; |
| 79 | + } |
| 80 | + } else { |
| 81 | + // No frontmatter, add it |
| 82 | + newContent = `---\nsource_repo: ${process.env.SOURCE_REPO}\nsource_path: ${sourcePath}\nsource_branch: ${currentBranch}\nlast_synced: ${now}\n---\n\n${content}`; |
| 83 | + } |
| 84 | + |
| 85 | + fs.writeFileSync(filePath, newContent); |
| 86 | + } |
| 87 | +
|
| 88 | + // Copy README if requested |
| 89 | + if (process.env.INCLUDE_README === 'true' && fs.existsSync('README.md')) { |
| 90 | + core.info('📄 Including README.md'); |
| 91 | + fs.copyFileSync('README.md', path.join(artifactDir, 'README.md')); |
| 92 | + addFrontmatter(path.join(artifactDir, 'README.md'), 'README.md'); |
| 93 | + } |
| 94 | +
|
| 95 | + // Copy documentation directory if it exists |
| 96 | + const docsPath = process.env.DOCS_PATH; |
| 97 | + if (fs.existsSync(docsPath)) { |
| 98 | + core.info(`📁 Copying documentation from ${docsPath}`); |
| 99 | + |
| 100 | + // Find and copy all markdown files |
| 101 | + const findCmd = `find "${docsPath}" -type f \\( -name "*.md" -o -name "*.mdx" \\)`; |
| 102 | + const files = execSync(findCmd).toString().trim().split('\n').filter(f => f); |
| 103 | + |
| 104 | + for (const file of files) { |
| 105 | + const relPath = file.replace(`${docsPath}/`, ''); |
| 106 | + const targetFile = path.join(artifactDir, relPath); |
| 107 | + |
| 108 | + // Create directory structure |
| 109 | + fs.mkdirSync(path.dirname(targetFile), { recursive: true }); |
| 110 | + |
| 111 | + // Copy file |
| 112 | + fs.copyFileSync(file, targetFile); |
| 113 | + |
| 114 | + // Add frontmatter |
| 115 | + const sourcePath = `${docsPath}/${relPath}`; |
| 116 | + addFrontmatter(targetFile, sourcePath); |
| 117 | + |
| 118 | + core.info(` ✅ Copied: ${relPath}`); |
| 119 | + } |
| 120 | + } else { |
| 121 | + core.warning(`⚠️ Documentation path ${docsPath} not found`); |
| 122 | + } |
| 123 | +
|
| 124 | + // Create index file |
| 125 | + const now = new Date().toISOString(); |
| 126 | + const repositoryName = process.env.SOURCE_REPO.split('/')[1] |
| 127 | + .replace(/[-_]+/g, ' ') |
| 128 | + .replace(/\b\w/g, ch => ch.toUpperCase()); |
| 129 | + const indexContent = `--- |
| 130 | + title: ${repositoryName} |
| 131 | + description: Documentation for ${repositoryName} |
| 132 | + --- |
| 133 | +
|
| 134 | + # ${repositoryName} |
| 135 | +
|
| 136 | + Documentation for the ${repositoryName} project. |
| 137 | +
|
| 138 | + **Source Repository:** [${process.env.SOURCE_REPO}](https://github.com/${process.env.SOURCE_REPO}) |
| 139 | + **Branch:** ${currentBranch} |
| 140 | + **Last Synced:** ${now} |
| 141 | + `; |
| 142 | + fs.writeFileSync(path.join(artifactDir, '_index.md'), indexContent); |
| 143 | +
|
| 144 | + // Show summary |
| 145 | + core.info('📊 Documentation bundle prepared:'); |
| 146 | + const allFiles = execSync(`find "${artifactDir}" -type f`).toString().trim().split('\n'); |
| 147 | + for (const file of allFiles) { |
| 148 | + core.info(` - ${file.replace(`${artifactDir}/`, '')}`); |
| 149 | + } |
| 150 | +
|
| 151 | + // Save artifact name for later use |
| 152 | + const slugifiedRepo = process.env.SOURCE_REPO.replace('/', '-').toLowerCase(); |
| 153 | + const artifactName = `docs-${slugifiedRepo}-${github.run_id}`; |
| 154 | + return artifactName; |
| 155 | +
|
| 156 | + - name: Upload documentation artifact |
| 157 | + id: upload-artifact |
| 158 | + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 |
| 159 | + with: |
| 160 | + name: ${{ steps.prepare.outputs.result }} |
| 161 | + path: documentation-artifact/ |
| 162 | + retention-days: 1 |
| 163 | + |
| 164 | + - name: Dispatch to public-docs |
| 165 | + uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0 |
| 166 | + with: |
| 167 | + token: ${{ secrets.PUBLIC_DOCS_TOKEN }} |
| 168 | + repository: hoverkraft-tech/public-docs |
| 169 | + event-type: sync-documentation |
| 170 | + client-payload: | |
| 171 | + { |
| 172 | + "repository": "${{ github.repository }}", |
| 173 | + "run-id": "${{ github.run_id }}", |
| 174 | + "artifact-id": "${{ steps.upload-artifact.outputs.artifact-id }}", |
| 175 | + } |
| 176 | +
|
| 177 | + - name: Summary |
| 178 | + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 |
| 179 | + env: |
| 180 | + DOCS_PATH: ${{ inputs.docs_path }} |
| 181 | + INCLUDE_README: ${{ inputs.include_readme }} |
| 182 | + ARTIFACT_NAME: ${{ steps.prepare.outputs.result }} |
| 183 | + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} |
| 184 | + with: |
| 185 | + script: | |
| 186 | + await core.summary |
| 187 | + .addHeading('📤 Documentation Dispatch Summary', 2) |
| 188 | + .addRaw('\n') |
| 189 | + .addList([ |
| 190 | + `**Docs Path**: ${process.env.DOCS_PATH}`, |
| 191 | + `**Include README**: ${process.env.INCLUDE_README}`, |
| 192 | + `**Artifact**: ${process.env.ARTIFACT_NAME} (ID: ${process.env.ARTIFACT_ID})`, |
| 193 | + ]) |
| 194 | + .addRaw('\n') |
| 195 | + .addRaw('The public-docs repository will download the artifact and publish the documentation to the portal.') |
| 196 | + .write(); |
0 commit comments