From 4f88bb8e5f16bacdbcda214649f62158ad8b1f40 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 8 Nov 2021 13:35:57 -0600 Subject: [PATCH] Antora Playbook --- .github/actions/algolia-config.json | 20 ++ .github/actions/algolia-deploy.sh | 20 ++ .github/actions/algolia-docsearch-scraper.sh | 21 ++ .github/actions/dispatch.sh | 4 +- .github/workflows/algolia-index.yml | 16 ++ ...uild-reference.yml => antora-generate.yml} | 4 +- .github/workflows/deploy-reference.yml | 33 +++ docs/antora-playbook.yml | 26 +++ docs/antora/extensions/major-minor-segment.js | 200 ++++++++++++++++++ docs/antora/extensions/root-component-name.js | 40 ++++ docs/local-antora-playbook.yml | 26 +++ docs/spring-security-docs.gradle | 17 ++ 12 files changed, 423 insertions(+), 4 deletions(-) create mode 100644 .github/actions/algolia-config.json create mode 100755 .github/actions/algolia-deploy.sh create mode 100755 .github/actions/algolia-docsearch-scraper.sh create mode 100644 .github/workflows/algolia-index.yml rename .github/workflows/{build-reference.yml => antora-generate.yml} (91%) create mode 100644 .github/workflows/deploy-reference.yml create mode 100644 docs/antora-playbook.yml create mode 100644 docs/antora/extensions/major-minor-segment.js create mode 100644 docs/antora/extensions/root-component-name.js create mode 100644 docs/local-antora-playbook.yml diff --git a/.github/actions/algolia-config.json b/.github/actions/algolia-config.json new file mode 100644 index 0000000000..09d30d486e --- /dev/null +++ b/.github/actions/algolia-config.json @@ -0,0 +1,20 @@ +{ + "index_name": "security-docs", + "start_urls": [ + "https://docs.spring.io/spring-security/reference/" + ], + "selectors": { + "lvl0": { + "selector": "//nav[@class='crumbs']//li[@class='crumb'][last()-1]", + "type": "xpath", + "global": true, + "default_value": "Home" + }, + "lvl1": ".doc h1", + "lvl2": ".doc h2", + "lvl3": ".doc h3", + "lvl4": ".doc h4", + "text": ".doc p, .doc td.content, .doc th.tableblock" + } +} + diff --git a/.github/actions/algolia-deploy.sh b/.github/actions/algolia-deploy.sh new file mode 100755 index 0000000000..994dfee9ac --- /dev/null +++ b/.github/actions/algolia-deploy.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +HOST="$1" +HOST_PATH="$2" +SSH_PRIVATE_KEY="$3" +SSH_KNOWN_HOST="$4" + + +if [ "$#" -ne 4 ]; then + echo -e "not enough arguments USAGE:\n\n$0 \$HOST \$HOST_PATH \$SSH_PRIVATE_KEY \$SSH_KNOWN_HOSTS \n\n" >&2 + exit 1 +fi + +# Use a non-default path to avoid overriding when testing locally +SSH_PRIVATE_KEY_PATH=~/.ssh/github-actions-docs +install -m 600 -D /dev/null "$SSH_PRIVATE_KEY_PATH" +echo "$SSH_PRIVATE_KEY" > "$SSH_PRIVATE_KEY_PATH" +echo "$SSH_KNOWN_HOST" > ~/.ssh/known_hosts +rsync --delete -avze "ssh -i $SSH_PRIVATE_KEY_PATH" docs/build/site/ "$HOST:$HOST_PATH" +rm -f "$SSH_PRIVATE_KEY_PATH" diff --git a/.github/actions/algolia-docsearch-scraper.sh b/.github/actions/algolia-docsearch-scraper.sh new file mode 100755 index 0000000000..2bb9ce178a --- /dev/null +++ b/.github/actions/algolia-docsearch-scraper.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +### +# Docs +# config.json https://docsearch.algolia.com/docs/config-file +# Run the crawler https://docsearch.algolia.com/docs/run-your-own/#run-the-crawl-from-the-docker-image + +### USAGE +if [ "$#" -ne 3 ]; then + echo -e "not enough arguments USAGE:\n\n$0 \$ALGOLIA_APPLICATION_ID \$ALGOLIA_API_KEY \$CONFIG_FILE\n\n" >&2 + exit 1 +fi + +# Script Parameters +APPLICATION_ID=$1 +API_KEY=$2 +CONFIG_FILE=$3 + +#### Script +script_dir=$(dirname $0) +docker run -e "APPLICATION_ID=$APPLICATION_ID" -e "API_KEY=$API_KEY" -e "CONFIG=$(cat $CONFIG_FILE | jq -r tostring)" algolia/docsearch-scraper diff --git a/.github/actions/dispatch.sh b/.github/actions/dispatch.sh index d6c2a37794..955e9cbbee 100755 --- a/.github/actions/dispatch.sh +++ b/.github/actions/dispatch.sh @@ -1,5 +1,5 @@ REPOSITORY_REF="$1" TOKEN="$2" -curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${TOKEN}" --request POST --data '{"event_type": "request-build"}' https://api.github.com/repos/${REPOSITORY_REF}/dispatches -echo "Requested Build for $REPOSITORY_REF" \ No newline at end of file +curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${TOKEN}" --request POST --data '{"event_type": "request-build-reference"}' https://api.github.com/repos/${REPOSITORY_REF}/dispatches +echo "Requested Build for $REPOSITORY_REF" diff --git a/.github/workflows/algolia-index.yml b/.github/workflows/algolia-index.yml new file mode 100644 index 0000000000..dfc2295af3 --- /dev/null +++ b/.github/workflows/algolia-index.yml @@ -0,0 +1,16 @@ +name: Update Algolia Index + +on: + schedule: + - cron: '0 10 * * *' # Once per day at 10am UTC + workflow_dispatch: # Manual trigger + +jobs: + update: + name: Update Algolia Index + runs-on: ubuntu-latest + steps: + - name: Checkout Source + uses: actions/checkout@v2 + - name: Update Index + run: ${GITHUB_WORKSPACE}/.github/actions/algolia-docsearch-scraper.sh "${{ secrets.ALGOLIA_APPLICATION_ID }}" "${{ secrets.ALGOLIA_WRITE_API_KEY }}" "${GITHUB_WORKSPACE}/.github/actions/algolia-config.json" diff --git a/.github/workflows/build-reference.yml b/.github/workflows/antora-generate.yml similarity index 91% rename from .github/workflows/build-reference.yml rename to .github/workflows/antora-generate.yml index 7387f4a040..f5cd25cfbf 100644 --- a/.github/workflows/build-reference.yml +++ b/.github/workflows/antora-generate.yml @@ -1,4 +1,4 @@ -name: reference +name: Generate Antora Files and Request Build on: push: @@ -27,4 +27,4 @@ jobs: repository-name: "spring-io/spring-generated-docs" token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - name: Dispatch Build Request - run: ${GITHUB_WORKSPACE}/.github/actions/dispatch.sh 'spring-io/spring-reference' "$GH_ACTIONS_REPO_TOKEN" + run: ${GITHUB_WORKSPACE}/.github/actions/dispatch.sh 'spring-projects/spring-security' "$GH_ACTIONS_REPO_TOKEN" diff --git a/.github/workflows/deploy-reference.yml b/.github/workflows/deploy-reference.yml new file mode 100644 index 0000000000..a0033b926b --- /dev/null +++ b/.github/workflows/deploy-reference.yml @@ -0,0 +1,33 @@ +name: Build & Deploy Reference + +on: + repository_dispatch: + types: request-build-reference + schedule: + - cron: '0 10 * * *' # Once per day at 10am UTC + workflow_dispatch: # Manual trigger + +jobs: + deploy: + name: deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + cache: gradle + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - name: Build with Gradle + run: ./gradlew :spring-security-docs:antora --stacktrace + - name: Cleanup Gradle Cache + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties + - name: Deploy + run: ${GITHUB_WORKSPACE}/.github/actions/algolia-deploy.sh "${{ secrets.DOCS_USERNAME }}@${{ secrets.DOCS_HOST }}" "/opt/www/domains/spring.io/docs/htdocs/spring-security/reference/" "${{ secrets.DOCS_SSH_KEY }}" "${{ secrets.DOCS_SSH_HOST_KEY }}" diff --git a/docs/antora-playbook.yml b/docs/antora-playbook.yml new file mode 100644 index 0000000000..519d53d195 --- /dev/null +++ b/docs/antora-playbook.yml @@ -0,0 +1,26 @@ +site: + title: Spring Security + url: https://docs.spring.io/spring-security/reference/ +asciidoc: + attributes: + page-pagination: true +content: + sources: + - url: https://github.com/spring-io/spring-generated-docs + branches: [spring-projects/spring-security/*] + - url: https://github.com/spring-projects/spring-security + branches: [main,5.6.x] + start_path: docs +urls: + latest_version_segment_strategy: redirect:to + latest_version_segment: '' + redirect_facility: httpd +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip + snapshot: true + +pipeline: + extensions: + - require: ./antora/extensions/major-minor-segment.js + - require: ./antora/extensions/root-component-name.js diff --git a/docs/antora/extensions/major-minor-segment.js b/docs/antora/extensions/major-minor-segment.js new file mode 100644 index 0000000000..eec0764544 --- /dev/null +++ b/docs/antora/extensions/major-minor-segment.js @@ -0,0 +1,200 @@ +// https://gitlab.com/antora/antora/-/issues/132#note_712132072 +'use strict' + +const { posix: path } = require('path') + +module.exports.register = (pipeline, { config }) => { + pipeline.on('contentClassified', ({ contentCatalog }) => { + contentCatalog.getComponents().forEach(component => { + const componentName = component.name; + const generationToVersion = new Map(); + component.versions.forEach(version => { + const generation = getGeneration(version.version); + const original = generationToVersion.get(generation); + if (original === undefined || (original.prerelease && !version.prerelease)) { + generationToVersion.set(generation, version); + } + }); + + const versionToGeneration = Array.from(generationToVersion.entries()).reduce((acc, entry) => { + const [ generation, version ] = entry; + acc.set(version.version, generation); + return acc; + }, new Map()); + + contentCatalog.findBy({ component: componentName }).forEach((file) => { + const candidateVersion = file.src.version; + if (versionToGeneration.has(candidateVersion)) { + const generation = versionToGeneration.get(candidateVersion); + if (file.out) { + if (file.out) { + file.out.dirname = file.out.dirname.replace(candidateVersion, generation) + file.out.path = file.out.path.replace(candidateVersion, generation); + } + } + if (file.pub) { + file.pub.url = file.pub.url.replace(candidateVersion, generation) + } + } + }); + versionToGeneration.forEach((generation, mappedVersion) => { + contentCatalog.getComponent(componentName).versions.filter(version => version.version === mappedVersion).forEach((version) => { + version.url = version.url.replace(mappedVersion, generation); + }) + const symbolicVersionAlias = createSymbolicVersionAlias( + componentName, + mappedVersion, + generation, + 'redirect:to' + ) + symbolicVersionAlias.src.version = generation; + contentCatalog.addFile(symbolicVersionAlias); + }); + }) + }) +} + +function createSymbolicVersionAlias (component, version, symbolicVersionSegment, strategy) { + if (symbolicVersionSegment == null || symbolicVersionSegment === version) return + const family = 'alias' + const baseVersionAliasSrc = { component, module: 'ROOT', family, relative: '', basename: '', stem: '', extname: '' } + const symbolicVersionAliasSrc = Object.assign({}, baseVersionAliasSrc, { version: symbolicVersionSegment }) + const symbolicVersionAlias = { + src: symbolicVersionAliasSrc, + pub: computePub( + symbolicVersionAliasSrc, + computeOut(symbolicVersionAliasSrc, family, symbolicVersionSegment), + family + ), + } + const originalVersionAliasSrc = Object.assign({}, baseVersionAliasSrc, { version }) + const originalVersionSegment = computeVersionSegment(component, version, 'original') + const originalVersionAlias = { + src: originalVersionAliasSrc, + pub: computePub( + originalVersionAliasSrc, + computeOut(originalVersionAliasSrc, family, originalVersionSegment), + family + ), + } + if (strategy === 'redirect:to') { + originalVersionAlias.out = undefined + originalVersionAlias.rel = symbolicVersionAlias + return originalVersionAlias + } else { + symbolicVersionAlias.out = undefined + symbolicVersionAlias.rel = originalVersionAlias + return symbolicVersionAlias + } +} + + +function computeOut (src, family, version, htmlUrlExtensionStyle) { + let { component, module: module_, basename, extname, relative, stem } = src + if (module_ === 'ROOT') module_ = '' + let indexifyPathSegment = '' + let familyPathSegment = '' + + if (family === 'page') { + if (stem !== 'index' && htmlUrlExtensionStyle === 'indexify') { + basename = 'index.html' + indexifyPathSegment = stem + } else if (extname === '.adoc') { + basename = stem + '.html' + } + } else if (family === 'image') { + familyPathSegment = '_images' + } else if (family === 'attachment') { + familyPathSegment = '_attachments' + } + const modulePath = path.join(component, version, module_) + const dirname = path.join(modulePath, familyPathSegment, path.dirname(relative), indexifyPathSegment) + const path_ = path.join(dirname, basename) + const moduleRootPath = path.relative(dirname, modulePath) || '.' + const rootPath = path.relative(dirname, '') || '.' + + return { dirname, basename, path: path_, moduleRootPath, rootPath } +} + +function computePub (src, out, family, version, htmlUrlExtensionStyle) { + const pub = {} + let url + if (family === 'nav') { + const urlSegments = version ? [src.component, version] : [src.component] + if (src.module && src.module !== 'ROOT') urlSegments.push(src.module) + // an artificial URL used for resolving page references in navigation model + url = '/' + urlSegments.join('/') + '/' + pub.moduleRootPath = '.' + } else if (family === 'page') { + const urlSegments = out.path.split('/') + const lastUrlSegmentIdx = urlSegments.length - 1 + if (htmlUrlExtensionStyle === 'drop') { + // drop just the .html extension or, if the filename is index.html, the whole segment + const lastUrlSegment = urlSegments[lastUrlSegmentIdx] + urlSegments[lastUrlSegmentIdx] = + lastUrlSegment === 'index.html' ? '' : lastUrlSegment.substr(0, lastUrlSegment.length - 5) + } else if (htmlUrlExtensionStyle === 'indexify') { + urlSegments[lastUrlSegmentIdx] = '' + } + url = '/' + urlSegments.join('/') + } else { + url = '/' + out.path + if (family === 'alias' && !src.relative.length) pub.splat = true + } + + pub.url = ~url.indexOf(' ') ? url.replace(SPACE_RX, '%20') : url + + if (out) { + pub.moduleRootPath = out.moduleRootPath + pub.rootPath = out.rootPath + } + + return pub +} + +function computeVersionSegment (name, version, mode) { + if (mode === 'original') return !version || version === 'master' ? '' : version + const strategy = this.latestVersionUrlSegmentStrategy + // NOTE: special exception; revisit in Antora 3 + if (!version || version === 'master') { + if (mode !== 'alias') return '' + if (strategy === 'redirect:to') return + } + if (strategy === 'redirect:to' || strategy === (mode === 'alias' ? 'redirect:from' : 'replace')) { + const component = this.getComponent(name) + const componentVersion = component && this.getComponentVersion(component, version) + if (componentVersion) { + const segment = + componentVersion === component.latest + ? this.latestVersionUrlSegment + : componentVersion === component.latestPrerelease + ? this.latestPrereleaseVersionUrlSegment + : undefined + return segment == null ? version : segment + } + } + return version +} + +function getGeneration(version) { + if (!version) return version; + const firstIndex = version.indexOf('.') + if (firstIndex < 0) { + return version; + } + const secondIndex = version.indexOf('.', firstIndex + 1); + const result = version.substr(0, secondIndex); + return result; +} + +function out(args) { + console.log(JSON.stringify(args, no_data, 2)); +} + + +function no_data(key, value) { + if (key == "data" || key == "files") { + return value ? "__data__" : value; + } + return value; +} \ No newline at end of file diff --git a/docs/antora/extensions/root-component-name.js b/docs/antora/extensions/root-component-name.js new file mode 100644 index 0000000000..dcc8dc482c --- /dev/null +++ b/docs/antora/extensions/root-component-name.js @@ -0,0 +1,40 @@ +// https://gitlab.com/antora/antora/-/issues/132#note_712132072 +'use strict' + +const { posix: path } = require('path') + +module.exports.register = (pipeline, { config }) => { + pipeline.on('contentClassified', ({ contentCatalog }) => { + const rootComponentName = config.rootComponentName || 'ROOT' + const rootComponentNameLength = rootComponentName.length + contentCatalog.findBy({ component: rootComponentName }).forEach((file) => { + if (file.out) { + file.out.dirname = file.out.dirname.substr(rootComponentNameLength) + file.out.path = file.out.path.substr(rootComponentNameLength + 1) + file.out.rootPath = fixPath(file.out.rootPath) + } + if (file.pub) { + file.pub.url = file.pub.url.substr(rootComponentNameLength + 1) + if (file.pub.rootPath) { + file.pub.rootPath = fixPath(file.pub.rootPath) + } + } + if (file.rel) { + if (file.rel.pub) { + file.rel.pub.url = file.rel.pub.url.substr(rootComponentNameLength + 1) + file.rel.pub.rootPath = fixPath(file.rel.pub.rootPath); + } + } + }) + const rootComponent = contentCatalog.getComponent(rootComponentName) + rootComponent?.versions?.forEach((version) => { + version.url = version.url.substr(rootComponentName.length + 1) + }) + // const siteStartPage = contentCatalog.getById({ component: '', version: '', module: '', family: 'alias', relative: 'index.adoc' }) + // if (siteStartPage) delete siteStartPage.out + }) + + function fixPath(path) { + return path.split('/').slice(1).join('/') || '.' + } +} \ No newline at end of file diff --git a/docs/local-antora-playbook.yml b/docs/local-antora-playbook.yml new file mode 100644 index 0000000000..8e2678cb29 --- /dev/null +++ b/docs/local-antora-playbook.yml @@ -0,0 +1,26 @@ +site: + title: Spring Security + url: https://docs.spring.io/spring-security/reference/ +asciidoc: + attributes: + page-pagination: true +content: + sources: + - url: ../../spring-io/spring-generated-docs + branches: [spring-projects/spring-security/*] + - url: ../../spring-projects/spring-security + branches: [main,5.6.x] + start_path: docs +urls: + latest_version_segment_strategy: redirect:to + latest_version_segment: '' + redirect_facility: httpd +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip + snapshot: true + +pipeline: + extensions: + - require: ./antora/extensions/major-minor-segment.js + - require: ./antora/extensions/root-component-name.js diff --git a/docs/spring-security-docs.gradle b/docs/spring-security-docs.gradle index 0c4747cd59..a28a20c5b3 100644 --- a/docs/spring-security-docs.gradle +++ b/docs/spring-security-docs.gradle @@ -1,6 +1,23 @@ +plugins { + id "io.github.rwinch.antora" version "0.0.2" +} + apply plugin: 'io.spring.convention.docs' apply plugin: 'java' +antora { + antoraVersion = "3.0.0-alpha.8" + arguments = ["--fetch"] +} + +tasks.antora { + environment = [ + "ALGOLIA_API_KEY" : "82c7ead946afbac3cf98c32446154691", + "ALGOLIA_APP_ID" : "244V8V9FGG", + "ALGOLIA_INDEX_NAME" : "security-docs" + ] +} + tasks.register("generateAntora") { group = "Documentation" description = "Generates the antora.yml for dynamic properties"