소스 검색

Antora Playbook

Rob Winch 3 년 전
부모
커밋
127e10e607

+ 20 - 0
.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"
+  }
+}
+

+ 20 - 0
.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"

+ 21 - 0
.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

+ 2 - 2
.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"
+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"

+ 16 - 0
.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"

+ 2 - 2
.github/workflows/build-reference.yml → .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"

+ 33 - 0
.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 }}"

+ 26 - 0
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

+ 1 - 1
docs/antora.yml

@@ -1,2 +1,2 @@
 name: ROOT
-version: 5.6
+version: '5.6'

+ 200 - 0
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;
+}

+ 40 - 0
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('/') || '.'
+    }
+}

+ 26 - 0
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

+ 17 - 0
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"