Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion .github/scripts/__tests__/antora-ui-bundle.test.js
Original file line number Diff line number Diff line change
@@ -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).
Expand Down Expand Up @@ -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({
Expand Down
15 changes: 14 additions & 1 deletion .github/scripts/antora-ui-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,26 @@ 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
// shape rather than by position.
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) {
Expand All @@ -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 };
11 changes: 11 additions & 0 deletions .github/workflows/README-update-antora-ui-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<tag>`. 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
Expand Down
42 changes: 35 additions & 7 deletions .github/workflows/update-antora-ui-bundle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'));

Expand All @@ -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}`);
Expand Down Expand Up @@ -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
Expand All @@ -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 = {}) => {
Expand All @@ -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 };

Expand Down Expand Up @@ -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) {
Expand All @@ -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 });
Expand All @@ -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, {
Expand Down
Loading