|
| 1 | +name: Aggregate Docs |
| 2 | + |
| 3 | +on: |
| 4 | + push: |
| 5 | + branches: [main] |
| 6 | + repository_dispatch: |
| 7 | + types: [docs-updated] |
| 8 | + schedule: |
| 9 | + - cron: '0 0 * * *' |
| 10 | + workflow_dispatch: |
| 11 | + |
| 12 | +concurrency: |
| 13 | + group: aggregate-docs |
| 14 | + cancel-in-progress: true |
| 15 | + |
| 16 | +permissions: |
| 17 | + contents: write |
| 18 | + |
| 19 | +jobs: |
| 20 | + aggregate: |
| 21 | + runs-on: ubuntu-latest |
| 22 | + steps: |
| 23 | + - uses: actions/checkout@v4 |
| 24 | + |
| 25 | + - uses: actions/github-script@v8 |
| 26 | + name: Aggregate docs from source repos |
| 27 | + env: |
| 28 | + PUSH_TOKEN: ${{ secrets.DOCS_PUSH_TOKEN }} |
| 29 | + with: |
| 30 | + script: | |
| 31 | + const fs = require('fs'); |
| 32 | + const path = require('path'); |
| 33 | +
|
| 34 | + // Read config files |
| 35 | + const repos = JSON.parse(fs.readFileSync('repos.json', 'utf8')); |
| 36 | + const docsConfig = JSON.parse(fs.readFileSync('docs.json', 'utf8')); |
| 37 | +
|
| 38 | + // Reset products array (start fresh each run) |
| 39 | + docsConfig.navigation.products = []; |
| 40 | +
|
| 41 | + // Create a temp working directory for the aggregated output |
| 42 | + const outDir = path.join(process.env.RUNNER_TEMP, 'aggregated'); |
| 43 | + await io.mkdirP(outDir); |
| 44 | +
|
| 45 | + // Copy base assets and config |
| 46 | + await io.cp('assets', path.join(outDir, 'assets'), { recursive: true }); |
| 47 | +
|
| 48 | + // Copy any top-level MDX pages from the docs repo itself |
| 49 | + for (const file of fs.readdirSync('.')) { |
| 50 | + if (file.endsWith('.mdx')) { |
| 51 | + await io.cp(file, path.join(outDir, file)); |
| 52 | + } |
| 53 | + } |
| 54 | +
|
| 55 | + // Process each source repo |
| 56 | + for (const { owner, repo, docsPath = 'docs', ref = 'main' } of repos) { |
| 57 | + core.startGroup(`Processing ${owner}/${repo}`); |
| 58 | +
|
| 59 | + const cloneDir = path.join(process.env.RUNNER_TEMP, 'repos', repo); |
| 60 | + await io.rmRF(cloneDir); |
| 61 | +
|
| 62 | + // Clone the repo (shallow, specific branch) |
| 63 | + await exec.exec('git', [ |
| 64 | + 'clone', '--depth=1', '--branch', ref, |
| 65 | + `https://x-access-token:${process.env.PUSH_TOKEN}@github.com/${owner}/${repo}.git`, |
| 66 | + cloneDir |
| 67 | + ]); |
| 68 | +
|
| 69 | + const sourceDir = path.join(cloneDir, docsPath); |
| 70 | + const sourceConfig = path.join(sourceDir, 'docs.json'); |
| 71 | +
|
| 72 | + if (!fs.existsSync(sourceConfig)) { |
| 73 | + core.warning(`No docs.json found in ${owner}/${repo}/${docsPath}, skipping`); |
| 74 | + core.endGroup(); |
| 75 | + continue; |
| 76 | + } |
| 77 | +
|
| 78 | + // Read the source repo's docs.json and merge navigation.products |
| 79 | + const subConfig = JSON.parse(fs.readFileSync(sourceConfig, 'utf8')); |
| 80 | + const subProducts = subConfig.navigation?.products ?? []; |
| 81 | + docsConfig.navigation.products.push(...subProducts); |
| 82 | +
|
| 83 | + // Copy all content except docs.json and assets/ |
| 84 | + const copyContents = (src, dest) => { |
| 85 | + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { |
| 86 | + if (entry.name === 'docs.json' || entry.name === 'assets') continue; |
| 87 | + const srcPath = path.join(src, entry.name); |
| 88 | + const destPath = path.join(dest, entry.name); |
| 89 | + if (entry.isDirectory()) { |
| 90 | + fs.mkdirSync(destPath, { recursive: true }); |
| 91 | + copyContents(srcPath, destPath); |
| 92 | + } else { |
| 93 | + fs.cpSync(srcPath, destPath); |
| 94 | + } |
| 95 | + } |
| 96 | + }; |
| 97 | +
|
| 98 | + copyContents(sourceDir, outDir); |
| 99 | + core.endGroup(); |
| 100 | + } |
| 101 | +
|
| 102 | + // Write the merged docs.json |
| 103 | + fs.writeFileSync( |
| 104 | + path.join(outDir, 'docs.json'), |
| 105 | + JSON.stringify(docsConfig, null, 4) + '\n' |
| 106 | + ); |
| 107 | +
|
| 108 | + core.info(`Aggregated ${docsConfig.navigation.products.length} product(s)`); |
| 109 | +
|
| 110 | + // Switch to the docs branch and replace contents |
| 111 | + const branch = 'docs'; |
| 112 | +
|
| 113 | + try { |
| 114 | + await exec.exec('git', ['fetch', 'origin', branch]); |
| 115 | + await exec.exec('git', ['checkout', branch]); |
| 116 | + } catch { |
| 117 | + await exec.exec('git', ['checkout', '--orphan', branch]); |
| 118 | + await exec.exec('git', ['rm', '-rf', '.']); |
| 119 | + } |
| 120 | +
|
| 121 | + // Clear the working directory (except .git) |
| 122 | + for (const entry of fs.readdirSync('.')) { |
| 123 | + if (entry === '.git') continue; |
| 124 | + await io.rmRF(entry); |
| 125 | + } |
| 126 | +
|
| 127 | + // Copy aggregated content into the working directory |
| 128 | + await io.cp(outDir, '.', { recursive: true, force: true }); |
| 129 | +
|
| 130 | + // Commit and push |
| 131 | + await exec.exec('git', ['add', '.']); |
| 132 | +
|
| 133 | + let hasChanges = false; |
| 134 | + try { |
| 135 | + await exec.exec('git', ['diff', '--cached', '--quiet']); |
| 136 | + } catch { |
| 137 | + hasChanges = true; |
| 138 | + } |
| 139 | +
|
| 140 | + if (!hasChanges) { |
| 141 | + core.info('No changes detected, skipping commit'); |
| 142 | + return; |
| 143 | + } |
| 144 | +
|
| 145 | + await exec.exec('git', ['config', 'user.name', 'github-actions[bot]']); |
| 146 | + await exec.exec('git', [ |
| 147 | + 'config', 'user.email', |
| 148 | + 'github-actions[bot]@users.noreply.github.com' |
| 149 | + ]); |
| 150 | + await exec.exec('git', ['commit', '-m', 'docs: aggregate from source repos']); |
| 151 | + await exec.exec('git', ['push', 'origin', branch]); |
| 152 | +
|
| 153 | + core.info('Docs aggregated and pushed successfully'); |
0 commit comments