diff --git a/.github/scripts/__tests__/antora-ui-bundle.test.js b/.github/scripts/__tests__/antora-ui-bundle.test.js index 8eb2c8b..88be1c7 100644 --- a/.github/scripts/__tests__/antora-ui-bundle.test.js +++ b/.github/scripts/__tests__/antora-ui-bundle.test.js @@ -1,4 +1,4 @@ -const { PLAYBOOK_PATH, extractBundle, rewrite, cmp } = require('../antora-ui-bundle'); +const { PLAYBOOK_PATH, DOCS_BUILD_BRANCH, DOCS_BUILD_PLAYBOOK_PATH, playbookCandidates, extractBundle, rewrite, cmp } = require('../antora-ui-bundle'); // The exact shape confirmed live on spring-cloud-commons's main, 4.2.x, 4.3.x and 5.0.x // branches (and their -commercial counterparts). @@ -28,6 +28,41 @@ describe('PLAYBOOK_PATH', () => { }); }); +describe('docs-build playbook', () => { + const DOCS_BUILD = [ + 'content:', + ' sources:', + ' - url: https://github.com/spring-cloud/spring-cloud-function', + ' branches: [ main, 4.3.x, 4.2.x ]', + 'ui:', + ' bundle:', + ' url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip', + ' snapshot: true', + '', + ].join('\n'); + + it('has the repo-wide branch and root playbook path', () => { + expect(DOCS_BUILD_BRANCH).toBe('docs-build'); + expect(DOCS_BUILD_PLAYBOOK_PATH).toBe('antora-playbook.yml'); + }); + + it('extracts and rewrites the bundle url without touching the rest', () => { + expect(extractBundle(DOCS_BUILD).tag).toBe('v0.4.18'); + const out = rewrite(DOCS_BUILD, { repo: 'spring-io/antora-ui-spring', tag: 'v0.4.26' }); + expect(out).toContain('releases/download/v0.4.26/ui-bundle.zip'); + expect(out.replace('v0.4.26', 'v0.4.18')).toBe(DOCS_BUILD); + }); +}); + +describe('playbookCandidates', () => { + it('tries .yml then .yaml, whichever the configured path uses', () => { + expect(playbookCandidates('antora-playbook.yml')) + .toEqual(['antora-playbook.yml', 'antora-playbook.yaml']); + expect(playbookCandidates('docs/antora-playbook.yaml')) + .toEqual(['docs/antora-playbook.yaml', 'docs/antora-playbook.yml']); + }); +}); + describe('extractBundle', () => { it('reads the repo, tag and full url from a real playbook', () => { expect(extractBundle(PLAYBOOK)).toEqual({ diff --git a/.github/scripts/antora-ui-bundle.js b/.github/scripts/antora-ui-bundle.js index dec5683..9b48317 100644 --- a/.github/scripts/antora-ui-bundle.js +++ b/.github/scripts/antora-ui-bundle.js @@ -8,6 +8,12 @@ const { cmp } = require('./version-cmp'); // such file at all. const PLAYBOOK_PATH = 'docs/antora-playbook.yml'; +// Every Antora project also keeps a repo-wide `docs-build` branch whose root +// antora-playbook.yml pins the same UI bundle URL (same shape, so extractBundle/rewrite apply +// unchanged). It is one branch per repository rather than one per maintained branch. +const DOCS_BUILD_BRANCH = 'docs-build'; +const DOCS_BUILD_PLAYBOOK_PATH = 'antora-playbook.yml'; + // Matches the UI bundle download URL wherever it appears in the file - matched by shape (a // GitHub release asset download link) rather than by indentation under ui:/bundle:/url:, // the same way currentMaven() in maven-wrapper-properties.js matches distributionUrl by @@ -15,6 +21,13 @@ const PLAYBOOK_PATH = 'docs/antora-playbook.yml'; const BUNDLE_URL = /https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/releases\/download\/([^/\s]+)\/([^\s'"]+)/; +// The path itself, then the same path with the other YAML extension - a playbook may be +// named either antora-playbook.yml or antora-playbook.yaml. +function playbookCandidates(path) { + const alt = path.endsWith('.yml') ? path.replace(/\.yml$/, '.yaml') : path.replace(/\.yaml$/, '.yml'); + return alt === path ? [path] : [path, alt]; +} + // { repo, tag, url } for the UI bundle release this playbook currently points at, or null // when no release-download URL is present at all. function extractBundle(text) { @@ -33,4 +46,4 @@ function rewrite(text, { repo, tag }) { return text.replace(current.url, newUrl); } -module.exports = { PLAYBOOK_PATH, extractBundle, rewrite, cmp }; +module.exports = { PLAYBOOK_PATH, DOCS_BUILD_BRANCH, DOCS_BUILD_PLAYBOOK_PATH, playbookCandidates, extractBundle, rewrite, cmp }; diff --git a/.github/workflows/README-update-antora-ui-bundle.md b/.github/workflows/README-update-antora-ui-bundle.md index e9981d2..35d2cfc 100644 --- a/.github/workflows/README-update-antora-ui-bundle.md +++ b/.github/workflows/README-update-antora-ui-bundle.md @@ -43,6 +43,17 @@ rather than there being one canonical file on the default branch. Branches that Antora adoption have no such file at all; those are reported as `no-playbook` rather than an error. +## The `docs-build` playbook + +Every Antora project also keeps a repo-wide `docs-build` branch whose root +`antora-playbook.yml` pins the same UI bundle URL. It is one branch per repository (not one +per maintained branch), so `setup` adds one extra matrix entry per repository — +`branch: docs-build`, `playbook_path: antora-playbook.yml` — and the `update` job handles it +through the same PR flow, statuses, auto-merge and notification as the per-branch playbooks. +The PR head is `antora-ui-bundle-update/docs-build-`. Either `.yml` or `.yaml` is accepted (`.yml` first), for both kinds of playbook. A repository with no +playbook on `docs-build` is reported as `no-playbook`. The `-internal` exclusion +does not apply. + ### What is out of scope - **`-internal` branches are skipped**, for the same reason diff --git a/.github/workflows/update-antora-ui-bundle.yml b/.github/workflows/update-antora-ui-bundle.yml index 9afcce4..a6407ca 100644 --- a/.github/workflows/update-antora-ui-bundle.yml +++ b/.github/workflows/update-antora-ui-bundle.yml @@ -17,6 +17,10 @@ run-name: "${{ (github.event_name != 'schedule' && inputs.dry_run != false) && ' # predate Antora adoption have no such file at all, which this discovers rather than assumes # (confirmed: spring-cloud-commons's 3.1.x has none, while main/4.2.x/4.3.x/5.0.x do). # +# Each repository's repo-wide docs-build branch keeps its own antora-playbook.yml (at the +# root) pinning the same bundle, so it gets one extra matrix entry per repository and is +# bumped through exactly the same PR flow. +# # This is the same shape of problem update-maven-wrapper.yml solves for the Maven wrapper - # an upstream artifact versions independently of every project, and nothing else notices when # it moves - and shares that workflow's gh-cli.js, git-pr-helpers.js, merge-if-green.js and @@ -129,10 +133,12 @@ jobs: PROJECTS_FILTER: ${{ inputs.projects }} REPO_TYPE: ${{ inputs.repo_type || 'both' }} MATRIX_LIB: ${{ github.workspace }}/.github/scripts/project-branch-matrix.js + BUNDLE_LIB: ${{ github.workspace }}/.github/scripts/antora-ui-bundle.js run: | node - << 'JSEOF' const fs = require('fs'); - const { buildBranchMatrix } = require(process.env.MATRIX_LIB); + const { buildBranchMatrix, walkProjects } = require(process.env.MATRIX_LIB); + const A = require(process.env.BUNDLE_LIB); const projects = JSON.parse(fs.readFileSync('config/projects.json', 'utf8')); @@ -147,6 +153,18 @@ jobs: repoType: process.env.REPO_TYPE, }); + // Plus one docs-build entry per repository: that branch is repo-wide (not one per + // maintained branch) and keeps its playbook at the root, so the entry carries its + // own path. A repo with no docs-build playbook is reported as no-playbook. + walkProjects(projects, { + filter: process.env.PROJECTS_FILTER, + repoType: process.env.REPO_TYPE, + }, ({ projectKey, typeKey, repo }) => { + entries.push({ project: projectKey, repo, type: typeKey, + branch: A.DOCS_BUILD_BRANCH, playbook_path: A.DOCS_BUILD_PLAYBOOK_PATH }); + }); + entries.sort((a, b) => a.repo.localeCompare(b.repo) || a.branch.localeCompare(b.branch)); + console.log(`Repo/branch combinations to check: ${entries.length}`); for (const e of entries) console.log(` ${e.repo}@${e.branch}`); console.log(`Excluded (-internal): ${excluded.length}`); @@ -189,6 +207,7 @@ jobs: BRANCH: ${{ matrix.branch }} PROJECT: ${{ matrix.project }} TYPE: ${{ matrix.type }} + PLAYBOOK_PATH: ${{ matrix.playbook_path }} TARGET_TAG: ${{ needs.versions.outputs.tag }} # A scheduled run has no inputs, so `inputs.dry_run != false` would be true and the # weekly job would never actually do anything. Schedule therefore acts, while a @@ -206,6 +225,10 @@ jobs: const BRANCH = process.env.BRANCH; const TARGET = process.env.TARGET_TAG; const DRY = process.env.DRY_RUN !== 'false'; + // docs/antora-playbook.yml on a maintained branch; antora-playbook.yml at the root + // of the repo-wide docs-build branch (the matrix entry says which). + // Either extension is accepted (.yml preferred), as update-antora-playbook does. + const CONFIGURED = process.env.PLAYBOOK_PATH || A.PLAYBOOK_PATH; const safe = `${REPO}@${BRANCH}`.replace(/[/@]/g, '-'); const finish = (status, detail, extra = {}) => { @@ -227,11 +250,16 @@ jobs: const enc = encodeURIComponent(BRANCH); // ── Read the playbook at this branch ───────────────────────────────────────────── - const got = PR.readFileAt(REPO, A.PLAYBOOK_PATH, enc); - if (!got) finish('no-playbook', `no ${A.PLAYBOOK_PATH} on this branch`); + let PLAYBOOK = CONFIGURED; + let got = null; + for (const candidate of A.playbookCandidates(CONFIGURED)) { + got = PR.readFileAt(REPO, candidate, enc); + if (got) { PLAYBOOK = candidate; break; } + } + if (!got) finish('no-playbook', `no ${CONFIGURED} (or .yaml) on this branch`); const bundle = A.extractBundle(got.text); - if (!bundle) finish('unparsed', `no UI bundle release-download url in ${A.PLAYBOOK_PATH}`); + if (!bundle) finish('unparsed', `no UI bundle release-download url in ${PLAYBOOK}`); const extras = { current: bundle.tag, target: TARGET }; @@ -273,7 +301,7 @@ jobs: // Read the PR's own head branch: it may already be at the target, or behind it // (a newer release has shipped since it was opened). const headRef = encodeURIComponent(pr.head.ref); - const onHead = PR.readFileAt(REPO, A.PLAYBOOK_PATH, headRef); + const onHead = PR.readFileAt(REPO, PLAYBOOK, headRef); const headBundle = onHead && A.extractBundle(onHead.text); if (!headBundle || A.cmp(headBundle.tag, TARGET) >= 0) { @@ -288,7 +316,7 @@ jobs: // PR per branch, always at the current target, rather than a growing stack. const headUpdated = A.rewrite(onHead.text, { repo: headBundle.repo, tag: TARGET }); const bump = PR.commitFilesToRef(REPO, pr.head.ref, - [{ path: A.PLAYBOOK_PATH, content: headUpdated }], message, author); + [{ path: PLAYBOOK, content: headUpdated }], message, author); if (!bump.ok) finish('error', `could not update #${pr.number}: ${bump.err}`); finish('pr-updated', `#${pr.number} moved up to ${TARGET}`, { ...extras, prUrl: pr.html_url, prNumber: pr.number }); @@ -307,7 +335,7 @@ jobs: const updatedText = A.rewrite(got.text, { repo: bundle.repo, tag: TARGET }); const put = PR.commitFilesToRef(REPO, head, - [{ path: A.PLAYBOOK_PATH, content: updatedText }], message, author); + [{ path: PLAYBOOK, content: updatedText }], message, author); if (!put.ok) finish('error', `could not commit: ${put.err}`); const opened = PR.openPr(REPO, {