Update provider submodules when docs change #41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Update provider submodules when docs change | |
| on: | |
| schedule: | |
| - cron: '0 7 * * *' # Daily 08:00 Berlin time (UTC+1) | |
| workflow_dispatch: # Manual trigger for testing | |
| jobs: | |
| check-and-update: | |
| name: Check for doc changes and update submodule | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| actions: write | |
| strategy: | |
| matrix: | |
| # To add a new provider: | |
| # 1. Register it as a git submodule in this repo: | |
| # git submodule add https://github.com/SAP/<provider-name>.git docs/<provider-name> | |
| # This updates .gitmodules and creates the submodule directory. | |
| # 2. Add an entry to the list below with: | |
| # - name: the submodule directory name (must match the path in .gitmodules) | |
| # - repo: the GitHub repo in "owner/repo" format | |
| # - submodule_path: relative path to the submodule in this repo (i.e. docs/<name>) | |
| # 3. Wire up the provider's docs in docusaurus.config.js and sidebars.js, | |
| # following the existing crossplane-provider-btp pattern. | |
| provider: | |
| - name: crossplane-provider-btp | |
| repo: SAP/crossplane-provider-btp | |
| submodule_path: docs/crossplane-provider-btp | |
| - name: crossplane-provider-hana | |
| repo: SAP/crossplane-provider-hana | |
| submodule_path: docs/crossplane-provider-hana | |
| steps: | |
| - name: Checkout docs repo | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - name: Configure git identity | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Check for docs changes and update submodule | |
| id: check | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const provider = '${{ matrix.provider.name }}'; | |
| const providerRepo = '${{ matrix.provider.repo }}'; | |
| const submodulePath = '${{ matrix.provider.submodule_path }}'; | |
| // 1. Get current submodule SHA | |
| const { execSync } = require('child_process'); | |
| const currentSha = execSync(`git -C ${submodulePath} rev-parse HEAD`).toString().trim(); | |
| console.log(`Current submodule SHA: ${currentSha}`); | |
| // 2. Get remote HEAD SHA | |
| const [owner, repo] = providerRepo.split('/'); | |
| const { data: branch } = await github.rest.repos.getBranch({ | |
| owner, repo, branch: 'main' | |
| }); | |
| const remoteSha = branch.commit.sha; | |
| console.log(`Remote HEAD SHA: ${remoteSha}`); | |
| // 3. No update needed? | |
| if (currentSha === remoteSha) { | |
| console.log('Submodule is up to date, nothing to do.'); | |
| core.setOutput('needs_update', 'false'); | |
| return; | |
| } | |
| // 4. Get file diff between current and remote SHA | |
| const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({ | |
| owner, repo, | |
| basehead: `${currentSha}...${remoteSha}` | |
| }); | |
| // 5. Check if any changed file is under docs/ | |
| const docFiles = comparison.files?.filter(f => f.filename.startsWith('docs/')) ?? []; | |
| console.log(`Doc files changed: ${docFiles.map(f => f.filename).join(', ') || 'none'}`); | |
| if (docFiles.length === 0) { | |
| console.log('No docs/** files changed between commits, skipping PR.'); | |
| core.setOutput('needs_update', 'false'); | |
| return; | |
| } | |
| // 6. Docs changed → set outputs for subsequent steps | |
| const shortSha = remoteSha.substring(0, 7); | |
| const branchName = `chore/update-${provider}-${shortSha}`; | |
| core.setOutput('needs_update', 'true'); | |
| core.setOutput('remote_sha', remoteSha); | |
| core.setOutput('short_sha', shortSha); | |
| core.setOutput('branch_name', branchName); | |
| core.setOutput('doc_files', docFiles.map(f => f.filename).join('\n')); | |
| console.log(`Docs changed! Will update submodule to ${remoteSha}`); | |
| - name: Close existing open PR for this provider (if any) | |
| if: steps.check.outputs.needs_update == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { data: prs } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| base: 'main' | |
| }); | |
| const provider = '${{ matrix.provider.name }}'; | |
| const existing = prs.find(pr => pr.head.ref.startsWith(`chore/update-${provider}-`)); | |
| if (existing) { | |
| await github.rest.pulls.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: existing.number, | |
| state: 'closed' | |
| }); | |
| console.log(`Closed superseded PR #${existing.number}`); | |
| await github.rest.git.deleteRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: `heads/${existing.head.ref}` | |
| }); | |
| console.log(`Deleted branch ${existing.head.ref}`); | |
| } | |
| - name: Create update branch and update submodule | |
| if: steps.check.outputs.needs_update == 'true' | |
| run: | | |
| git checkout -b "${{ steps.check.outputs.branch_name }}" | |
| cd "${{ matrix.provider.submodule_path }}" | |
| git fetch origin | |
| git checkout "${{ steps.check.outputs.remote_sha }}" | |
| cd - | |
| git add "${{ matrix.provider.submodule_path }}" | |
| git commit -m "chore: update ${{ matrix.provider.name }} docs to ${{ steps.check.outputs.short_sha }}" | |
| git push origin "${{ steps.check.outputs.branch_name }}" | |
| - name: Trigger test build and wait for result | |
| if: steps.check.outputs.needs_update == 'true' | |
| id: build | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const branchName = '${{ steps.check.outputs.branch_name }}'; | |
| // github-actions[bot] PRs do not trigger pull_request events — | |
| // GitHub suppresses them to prevent infinite loops. We dispatch | |
| // the build manually and wait for it to finish before opening the | |
| // PR, so the PR body already shows the final build status. | |
| await github.rest.actions.createWorkflowDispatch({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'test-build.yaml', | |
| ref: branchName, | |
| inputs: { ref: branchName } | |
| }); | |
| // Give GitHub 10s to register the run before we start polling. | |
| await new Promise(resolve => setTimeout(resolve, 10000)); | |
| // Poll every 20s, up to 30 attempts (10 min total). | |
| // A typical Docusaurus build finishes in ~2-3 min. | |
| let conclusion = null; | |
| let runUrl = null; | |
| for (let attempt = 0; attempt < 30; attempt++) { | |
| const { data: runs } = await github.rest.actions.listWorkflowRuns({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'test-build.yaml', | |
| branch: branchName, | |
| event: 'workflow_dispatch', | |
| per_page: 5 | |
| }); | |
| const run = runs.workflow_runs.find(r => r.status === 'completed' || r.status === 'in_progress' || r.status === 'queued'); | |
| if (run && run.status === 'completed') { | |
| conclusion = run.conclusion; | |
| runUrl = run.html_url; | |
| console.log(`Build completed: ${conclusion} — ${runUrl}`); | |
| break; | |
| } | |
| console.log(`Attempt ${attempt + 1}/30: status=${run?.status ?? 'not found'}, waiting 20s...`); | |
| await new Promise(resolve => setTimeout(resolve, 20000)); | |
| } | |
| if (!conclusion) { | |
| // Timed out after 10 min — open the PR anyway with unknown status. | |
| conclusion = 'unknown'; | |
| } | |
| core.setOutput('conclusion', conclusion); | |
| core.setOutput('run_url', runUrl ?? ''); | |
| - name: Open pull request | |
| if: steps.check.outputs.needs_update == 'true' | |
| id: open_pr | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const provider = '${{ matrix.provider.name }}'; | |
| const providerRepo = '${{ matrix.provider.repo }}'; | |
| const sha = '${{ steps.check.outputs.remote_sha }}'; | |
| const shortSha = '${{ steps.check.outputs.short_sha }}'; | |
| const branchName = '${{ steps.check.outputs.branch_name }}'; | |
| const docFiles = `${{ steps.check.outputs.doc_files }}`; | |
| const conclusion = '${{ steps.build.outputs.conclusion }}'; | |
| const runUrl = '${{ steps.build.outputs.run_url }}'; | |
| const statusIcon = conclusion === 'success' ? '✅ passed' | |
| : conclusion === 'failure' ? '❌ failed' | |
| : `⚠️ ${conclusion}`; | |
| const runLink = runUrl ? `[View run](${runUrl})` : '—'; | |
| const { data: pr } = await github.rest.pulls.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `chore: update ${provider} docs to ${shortSha}`, | |
| body: [ | |
| `## Automated documentation update`, | |
| ``, | |
| `This PR updates the \`${provider}\` submodule to commit [\`${shortSha}\`](https://github.com/${providerRepo}/commit/${sha}).`, | |
| ``, | |
| `**Changed doc files:**`, | |
| docFiles.split('\n').map(f => `- \`${f}\``).join('\n'), | |
| ``, | |
| `## Test results`, | |
| ``, | |
| `| Test | Status | Run |`, | |
| `| ---- | ------ | --- |`, | |
| `| Docusaurus build | ${statusIcon} | ${runLink} |`, | |
| ].join('\n'), | |
| head: branchName, | |
| base: 'main', | |
| draft: false | |
| }); | |
| core.setOutput('pr_number', pr.number); |