This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Docs (GitHub Pages) | |
| on: | |
| push: | |
| branches: [ main ] | |
| paths: | |
| - README.md | |
| - docs/** | |
| - github-pages/** | |
| - .github/workflows/jekyll-gh-pages.yml | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| concurrency: | |
| group: pages | |
| cancel-in-progress: false | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| - name: Setup Pages | |
| uses: actions/configure-pages@v5 | |
| with: | |
| enablement: true | |
| - name: Add version and build date | |
| run: | | |
| set -euo pipefail | |
| VERSION="$(python3 - <<'PY' | |
| import xml.etree.ElementTree as ET | |
| tree = ET.parse("Directory.Build.props") | |
| root = tree.getroot() | |
| def find_text(suffix: str) -> str: | |
| for element in root.iter(): | |
| if element.tag.endswith(suffix) and element.text and element.text.strip(): | |
| return element.text.strip() | |
| return "" | |
| version = find_text("PackageVersion") or find_text("Version") | |
| print(version) | |
| PY | |
| )" | |
| if [ -z "${VERSION}" ]; then | |
| echo "::error::Could not read <PackageVersion>/<Version> from Directory.Build.props" | |
| exit 1 | |
| fi | |
| echo "version: \"${VERSION}\"" >> github-pages/_config.yml | |
| echo "build_date: \"$(date +'%Y-%m-%d')\"" >> github-pages/_config.yml | |
| - name: Generate site pages from docs | |
| run: | | |
| set -euo pipefail | |
| REPO="${{ github.repository }}" | |
| REPO_BLOB_BASE="https://github.com/${REPO}/blob/main" | |
| REPO_TREE_BASE="https://github.com/${REPO}/tree/main" | |
| rewrite_md_links() { | |
| # Rewrite *.md links to *.html for the rendered site (keep source docs GitHub-friendly). | |
| # Only rewrites relative links (no ':' in the target) so external URLs are untouched. | |
| sed -E \ | |
| -e 's|\]\(README\.md\)|](/)|g' \ | |
| -e 's|\]\(docs/Development/setup\.md\)|](/setup/)|g' \ | |
| -e 's|\]\(docs/Development/credentials\.md\)|](/credentials/)|g' \ | |
| -e 's|\]\(docs/Testing/strategy\.md\)|](/testing/)|g' \ | |
| -e 's|\]\(docs/Features/index\.md\)|](/features/)|g' \ | |
| -e 's|\]\(docs/ADR/index\.md\)|](/adr/)|g' \ | |
| -e 's|\]\(docs/API/index\.md\)|](/api/)|g' \ | |
| -e 's|\]\(docs/API/([^):]+)\.md(#[^)]*)?\)|](/api/\\1.html\\2)|g' \ | |
| -e 's|\]\(docs/Features/([^):]+)\.md(#[^)]*)?\)|](/features/\\1.html\\2)|g' \ | |
| -e 's|\]\(docs/ADR/([^):]+)\.md(#[^)]*)?\)|](/adr/\\1.html\\2)|g' \ | |
| -e 's|\]\(\\.\\./Development/setup\.md\)|](/setup/)|g' \ | |
| -e 's|\]\(\\.\\./Development/credentials\.md\)|](/credentials/)|g' \ | |
| -e 's|\]\(\\.\\./Testing/strategy\.md\)|](/testing/)|g' \ | |
| -e 's|\]\(\\.\\./ADR/([^):]+)\.md(#[^)]*)?\)|](/adr/\\1.html\\2)|g' \ | |
| -e 's|\]\(\\.\\./Features/([^):]+)\.md(#[^)]*)?\)|](/features/\\1.html\\2)|g' \ | |
| -e 's|\]\(\\.\\./API/([^):]+)\.md(#[^)]*)?\)|](/api/\\1.html\\2)|g' \ | |
| -e 's|\]\(([^):]+)\.md(#[^)]*)?\)|](\1.html\2)|g' | |
| } | |
| linkify_repo_paths() { | |
| local script | |
| script="$(mktemp)" | |
| cat > "${script}" <<'PY' | |
| import re | |
| import sys | |
| blob_base = sys.argv[1].rstrip("/") | |
| tree_base = sys.argv[2].rstrip("/") | |
| prefixes = r"(?:\\.github/|github-pages/|docs/|Tests/|Storages/|Integraions/|ManagedCode\\.Storage\\.[^`/]+/)" | |
| root_files = r"(?:Directory\\.Build\\.props|ManagedCode\\.Storage\\.slnx|README\\.md|AGENTS\\.md|LICENSE)" | |
| text = sys.stdin.read() | |
| def link_tree(match: re.Match[str]) -> str: | |
| path = match.group(1) | |
| return f"[`{path}/`]({tree_base}/{path})" | |
| def link_blob(match: re.Match[str]) -> str: | |
| path = match.group(1) | |
| if path.endswith("/"): | |
| return match.group(0) | |
| last_segment = path.rsplit("/", 1)[-1] | |
| if "." not in last_segment: | |
| return match.group(0) | |
| return f"[`{path}`]({blob_base}/{path})" | |
| def link_root_file(match: re.Match[str]) -> str: | |
| path = match.group(1) | |
| return f"[`{path}`]({blob_base}/{path})" | |
| text = re.sub(rf"(?<!\[)`(({prefixes})[^`]*)/`", link_tree, text) | |
| text = re.sub(rf"(?<!\[)`(({prefixes})[^`]*)`", link_blob, text) | |
| text = re.sub(rf"(?<!\[)`({root_files})`", link_root_file, text) | |
| sys.stdout.write(text) | |
| PY | |
| python3 "${script}" "${REPO_BLOB_BASE}" "${REPO_TREE_BASE}" | |
| rm -f "${script}" | |
| } | |
| extract_description() { | |
| # Grab the first meaningful paragraph (skips headings, code fences, and bullet lists). | |
| awk ' | |
| BEGIN { in_code=0 } | |
| /^```/ { in_code = !in_code; next } | |
| in_code { next } | |
| NR == 1 { next } # skip H1 | |
| /^[[:space:]]*$/ { next } # skip blanks | |
| /^#/ { next } # skip headings | |
| /^(Accepted|Proposed|Rejected|Superseded|Deprecated)[[:space:]]/ { next } # skip ADR status line | |
| /^[[:space:]]*[-*]/ { next } # skip bullets | |
| { print; exit } | |
| ' | |
| } | |
| strip_front_matter() { | |
| awk ' | |
| NR==1 && $0=="---" { in_front=1; next } | |
| in_front && $0=="---" { in_front=0; next } | |
| !in_front { print } | |
| ' "$1" | |
| } | |
| read_front_matter_value() { | |
| local file="$1" | |
| local key="$2" | |
| awk -v key="$key" ' | |
| NR==1 && $0=="---" { in_front=1; next } | |
| in_front && $0=="---" { exit } | |
| in_front { | |
| if ($0 ~ ("^" key ":[[:space:]]*")) { | |
| sub("^" key ":[[:space:]]*", "", $0) | |
| gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0) | |
| if ($0 ~ /^".*"$/) { sub(/^"/, "", $0); sub(/"$/, "", $0) } | |
| print $0 | |
| exit | |
| } | |
| } | |
| ' "$file" | |
| } | |
| extract_h1() { | |
| awk '/^# / { sub(/^# /, ""); print; exit }' | |
| } | |
| write_doc_page() { | |
| local src="$1" | |
| local dst="$2" | |
| local default_permalink="${3-}" | |
| local default_nav_order="${4-}" | |
| local title desc keywords permalink nav_order | |
| title=$(read_front_matter_value "$src" "title") | |
| if [ -z "${title}" ]; then | |
| title=$(strip_front_matter "$src" | extract_h1) | |
| fi | |
| desc=$(read_front_matter_value "$src" "description") | |
| if [ -z "${desc}" ]; then | |
| desc=$(strip_front_matter "$src" | extract_description | tr -d '\r\n') | |
| desc=$(printf '%s' "$desc" | sed -E 's/[[:space:]]+/ /g; s/[[:space:]]+$//; s/:$//') | |
| if [ ${#desc} -gt 160 ]; then | |
| desc="${desc:0:159}…" | |
| fi | |
| if [ -z "${desc}" ]; then | |
| desc="${title} documentation." | |
| fi | |
| fi | |
| keywords=$(read_front_matter_value "$src" "keywords") | |
| permalink=$(read_front_matter_value "$src" "permalink") | |
| nav_order=$(read_front_matter_value "$src" "nav_order") | |
| if [ -z "${permalink}" ] && [ -n "${default_permalink}" ]; then | |
| permalink="$default_permalink" | |
| fi | |
| if [ -z "${nav_order}" ] && [ -n "${default_nav_order}" ]; then | |
| nav_order="$default_nav_order" | |
| fi | |
| local title_escaped desc_escaped keywords_escaped | |
| title_escaped=$(printf '%s' "$title" | sed 's/"/\\"/g') | |
| desc_escaped=$(printf '%s' "$desc" | sed 's/"/\\"/g') | |
| keywords_escaped=$(printf '%s' "$keywords" | sed 's/"/\\"/g') | |
| { | |
| echo "---" | |
| echo "layout: default" | |
| echo "title: \"${title_escaped}\"" | |
| echo "description: \"${desc_escaped}\"" | |
| if [ -n "${keywords}" ]; then | |
| echo "keywords: \"${keywords_escaped}\"" | |
| fi | |
| if [ -n "${permalink}" ]; then | |
| echo "permalink: ${permalink}" | |
| fi | |
| if [ -n "${nav_order}" ]; then | |
| echo "nav_order: ${nav_order}" | |
| fi | |
| echo "---" | |
| } > "$dst" | |
| strip_front_matter "$src" | rewrite_md_links | linkify_repo_paths >> "$dst" | |
| } | |
| mkdir -p github-pages/features | |
| WORDS=$(wc -w < README.md) | |
| MINUTES=$(( (WORDS + 200) / 200 )) | |
| { | |
| echo "---" | |
| echo "layout: default" | |
| echo "title: Home" | |
| echo "is_home: true" | |
| echo "permalink: /" | |
| echo "nav_order: 1" | |
| echo "---" | |
| } > github-pages/index.md | |
| rewrite_md_links < README.md | linkify_repo_paths >> github-pages/index.md | |
| echo "" >> github-pages/index.md | |
| echo "<p class=\"reading-time\">${MINUTES} min read</p>" >> github-pages/index.md | |
| write_doc_page docs/Development/setup.md github-pages/setup.md /setup/ 2 | |
| write_doc_page docs/Development/credentials.md github-pages/credentials.md /credentials/ 3 | |
| write_doc_page docs/Testing/strategy.md github-pages/testing.md /testing/ 4 | |
| write_doc_page docs/Features/index.md github-pages/features/index.md /features/ 5 | |
| for file in docs/Features/*.md; do | |
| base=$(basename "$file") | |
| if [ "$base" = "index.md" ]; then | |
| continue | |
| fi | |
| write_doc_page "$file" "github-pages/features/$base" | |
| done | |
| mkdir -p github-pages/adr | |
| write_doc_page docs/ADR/index.md github-pages/adr/index.md /adr/ 6 | |
| for file in docs/ADR/*.md; do | |
| base=$(basename "$file") | |
| if [ "$base" = "index.md" ]; then | |
| continue | |
| fi | |
| write_doc_page "$file" "github-pages/adr/$base" | |
| done | |
| mkdir -p github-pages/api | |
| write_doc_page docs/API/index.md github-pages/api/index.md /api/ 7 | |
| for file in docs/API/*.md; do | |
| base=$(basename "$file") | |
| if [ "$base" = "index.md" ]; then | |
| continue | |
| fi | |
| write_doc_page "$file" "github-pages/api/$base" | |
| done | |
| - name: Build with Jekyll | |
| uses: actions/jekyll-build-pages@v1 | |
| with: | |
| source: ./github-pages | |
| destination: ./_site | |
| - name: Upload artifact | |
| uses: actions/upload-pages-artifact@v4 | |
| deploy: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v4 |