Skip to content

Update provider submodules when docs change #37

Update provider submodules when docs change

Update provider submodules when docs change #37

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);