diff --git a/.github/scripts/validate-artifacts.py b/.github/scripts/validate-artifacts.py index eedcedc..03ce7b9 100644 --- a/.github/scripts/validate-artifacts.py +++ b/.github/scripts/validate-artifacts.py @@ -58,6 +58,18 @@ def ensure_exists(path: Path, label: str) -> None: print(f"OK [{label}] exists ({path.stat().st_size} bytes)") + + +def read_markdown_version(path: Path) -> str: + text = path.read_text(encoding="utf-8") + match = re.search(r'(?m)^version:\s*["\']?([^"\'\n]+)["\']?\s*$', text) + if not match: + raise ValidationError(f"Markdown frontmatter version not found: {path}") + version = normalize_text(match.group(1)) + if not version: + raise ValidationError(f"Markdown frontmatter version is empty: {path}") + return version + def extract_pdf_text(path: Path) -> str: try: result = subprocess.run( @@ -77,10 +89,10 @@ def extract_pdf_text(path: Path) -> str: return result.stdout -def validate_pdf(path: Path) -> None: +def validate_pdf(path: Path, expected_version: str) -> None: ensure_exists(path, "PDF") text = extract_pdf_text(path) - assert_contains(text, (FULL_TITLE, SHORT_TITLE, "Version 1.1", *CANONICAL_TOKENS), "PDF") + assert_contains(text, (FULL_TITLE, SHORT_TITLE, f"Version {expected_version}", *CANONICAL_TOKENS), "PDF") print("OK [PDF] canonical text markers found") return text @@ -178,9 +190,11 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() try: + markdown_path = Path(args.markdown) + expected_version = read_markdown_version(markdown_path) texts = { - "Markdown": validate_markdown(Path(args.markdown)), - "PDF": validate_pdf(Path(args.pdf)), + "Markdown": validate_markdown(markdown_path), + "PDF": validate_pdf(Path(args.pdf), expected_version), "EPUB": validate_epub(Path(args.epub)), "DOCX": validate_docx(Path(args.docx)), } diff --git a/.github/scripts/validate-release-metadata.py b/.github/scripts/validate-release-metadata.py new file mode 100644 index 0000000..c1cab21 --- /dev/null +++ b/.github/scripts/validate-release-metadata.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Validate release metadata for a single-document documentation repository.""" + +from __future__ import annotations + +import argparse +import re +import sys +from datetime import datetime +from pathlib import Path + +MONTH_PATTERN = re.compile( + r"^(January|February|March|April|May|June|July|August|September|October|November|December) ([1-9]|[12][0-9]|3[01]), (20[0-9]{2})$" +) +RELEASE_HEADING_PATTERN = re.compile(r"^## ([0-9]+\.[0-9]+(?:\.[0-9]+)?) — ((?:19|20)\d{2}-\d{2}-\d{2})$") + + +class ValidationError(Exception): + """Metadata validation failed.""" + + +def normalize_semver(version: str) -> str: + version = version.strip() + if not re.fullmatch(r"\d+\.\d+(?:\.\d+)?", version): + raise ValidationError(f"Version is not semver-like: {version!r}") + parts = version.split('.') + if len(parts) == 2: + parts.append('0') + return '.'.join(str(int(part)) for part in parts) + + +def parse_frontmatter(markdown_path: Path) -> tuple[str, str]: + text = markdown_path.read_text(encoding='utf-8') + if not text.startswith('---\n'): + raise ValidationError(f"Markdown file does not start with YAML frontmatter: {markdown_path}") + version_match = re.search(r'(?m)^version:\s*["\']?([^"\'\n]+)["\']?\s*$', text) + date_match = re.search(r'(?m)^date:\s*["\']?([^"\'\n]+)["\']?\s*$', text) + if not version_match: + raise ValidationError(f"Markdown frontmatter version not found: {markdown_path}") + if not date_match: + raise ValidationError(f"Markdown frontmatter date not found: {markdown_path}") + return version_match.group(1).strip(), date_match.group(1).strip() + + +def validate_pretty_date(value: str) -> str: + if not MONTH_PATTERN.fullmatch(value): + raise ValidationError(f"Date is not in 'Month D, YYYY' form: {value!r}") + return value + + +def iso_to_pretty(value: str) -> str: + dt = datetime.strptime(value, '%Y-%m-%d') + return f"{dt.strftime('%B')} {dt.day}, {dt.year}" + + +def parse_expected_date(value: str | None) -> str | None: + if not value: + return None + value = value.strip() + if re.fullmatch(r'(?:19|20)\d{2}-\d{2}-\d{2}', value): + return iso_to_pretty(value) + return validate_pretty_date(value) + + +def parse_release_headings(changelog_path: Path) -> list[tuple[str, str]]: + releases: list[tuple[str, str]] = [] + for line in changelog_path.read_text(encoding='utf-8').splitlines(): + match = RELEASE_HEADING_PATTERN.match(line.strip()) + if match: + version, iso_date = match.groups() + releases.append((version, iso_to_pretty(iso_date))) + if not releases: + raise ValidationError(f"No release heading found in {changelog_path}") + return releases + + +def parse_expected_tag(value: str | None) -> str | None: + if not value: + return None + return normalize_semver(value.removeprefix('refs/tags/').removeprefix('v')) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--markdown', required=True, help='Primary Markdown file') + parser.add_argument('--changelog', default='CHANGELOG.md', help='Changelog path') + parser.add_argument('--expected-tag', help='Expected git tag such as v1.2.0') + parser.add_argument('--expected-date', help='Expected release date, either YYYY-MM-DD or Month D, YYYY') + parser.add_argument('--strict-changelog', action='store_true', help='Require the changelog to contain a release heading matching the current version and date') + return parser.parse_args() + + +def main() -> int: + args = parse_args() + try: + raw_version, raw_date = parse_frontmatter(Path(args.markdown)) + pretty_date = validate_pretty_date(raw_date) + normalized_version = normalize_semver(raw_version) + releases = parse_release_headings(Path(args.changelog)) + latest_version, latest_date = releases[0] + matching_release = None + for release_version, release_date in releases: + if normalize_semver(release_version) == normalized_version: + matching_release = (release_version, release_date) + break + + if args.strict_changelog: + if not matching_release: + raise ValidationError( + f"No changelog release heading matches frontmatter version {raw_version!r}" + ) + if pretty_date != matching_release[1]: + raise ValidationError( + f"Frontmatter date {raw_date!r} does not match changelog release date {matching_release[1]!r}" + ) + + expected_tag = parse_expected_tag(args.expected_tag) + if expected_tag and normalized_version != expected_tag: + raise ValidationError( + f"Frontmatter version {normalized_version} does not match expected tag {expected_tag}" + ) + + expected_date = parse_expected_date(args.expected_date) + if expected_date and pretty_date != expected_date: + raise ValidationError( + f"Frontmatter date {pretty_date!r} does not match expected release date {expected_date!r}" + ) + + print(f"OK [Frontmatter version] raw={raw_version} normalized={normalized_version}") + print(f"OK [Frontmatter date] {pretty_date}") + print(f"OK [Latest changelog release] {latest_version} / {latest_date}") + if matching_release: + print(f"OK [Matching changelog release] {matching_release[0]} / {matching_release[1]}") + else: + print(f"OK [Matching changelog release] none for normalized version {normalized_version}") + if expected_tag: + print(f"OK [Expected tag] {expected_tag}") + if expected_date: + print(f"OK [Expected date] {expected_date}") + print('Release metadata validation passed.') + return 0 + except ValidationError as exc: + print(f"FAIL {exc}", file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/.github/workflows/generate-docs.yml b/.github/workflows/generate-docs.yml index 8820e8e..272b7f1 100644 --- a/.github/workflows/generate-docs.yml +++ b/.github/workflows/generate-docs.yml @@ -1,19 +1,18 @@ name: Generate PDF, Word & EPUB Documents on: - push: - branches: [main] - paths: - - 'WordPress-Security-Benchmark.md' - - '.github/workflows/generate-docs.yml' - - '.github/pandoc/**' workflow_dispatch: + inputs: + ref: + description: 'Branch, tag, or SHA to build (defaults to the selected ref)' + required: false + type: string permissions: - contents: write + contents: read concurrency: - group: generate-docs-${{ github.repository }} + group: generate-docs-${{ github.repository }}-${{ inputs.ref || github.ref }} cancel-in-progress: false jobs: @@ -25,6 +24,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ inputs.ref || github.ref }} - name: Install Pandoc run: | @@ -36,77 +37,41 @@ jobs: - name: Install LaTeX, fonts, and validators run: | sudo apt-get update -qq - sudo apt-get install -y \ - texlive-xetex \ - texlive-latex-recommended \ - texlive-fonts-recommended \ - texlive-fonts-extra \ - texlive-latex-extra \ - lmodern \ - fonts-noto-core \ - fonts-noto-extra \ - epubcheck \ - poppler-utils \ - unzip + sudo apt-get install -y texlive-xetex texlive-latex-recommended texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra lmodern fonts-noto-core fonts-noto-extra epubcheck poppler-utils unzip - name: Install eisvogel template run: | mkdir -p ~/.pandoc/templates TMPDIR_EIS=$(mktemp -d) - curl -sL "https://github.com/Wandmalfarbe/pandoc-latex-template/releases/latest/download/Eisvogel.tar.gz" \ - | tar -xz -C "$TMPDIR_EIS" + curl -sL "https://github.com/Wandmalfarbe/pandoc-latex-template/releases/latest/download/Eisvogel.tar.gz" | tar -xz -C "$TMPDIR_EIS" find "$TMPDIR_EIS" -name "*.latex" -exec cp {} ~/.pandoc/templates/ \; rm -rf "$TMPDIR_EIS" - name: Bootstrap pandoc styling assets run: | mkdir -p .github/pandoc - if [ ! -f .github/pandoc/reference.docx ]; then pandoc --print-default-data-file reference.docx > .github/pandoc/reference.docx fi - if [ ! -f .github/pandoc/pdf-defaults.yaml ]; then - { - printf '%s\n' '# Pandoc PDF defaults - eisvogel template, professional dark-header layout' - printf '%s\n' 'variables:' - printf '%s\n' ' mainfont: "Noto Serif"' - printf '%s\n' ' sansfont: "Noto Sans"' - printf '%s\n' ' monofont: "Noto Sans Mono"' - printf '%s\n' ' fontsize: "11pt"' - printf '%s\n' ' geometry: "margin=2.5cm, top=3cm, bottom=3cm"' - printf '%s\n' ' linestretch: 1.2' - printf '%s\n' ' titlepage: true' - printf '%s\n' ' titlepage-color: "1A1A2E"' - printf '%s\n' ' titlepage-text-color: "FFFFFF"' - printf '%s\n' ' titlepage-rule-color: "0073AA"' - printf '%s\n' ' titlepage-rule-height: 6' - printf '%s\n' ' toc-own-page: true' - printf '%s\n' ' colorlinks: true' - printf '%s\n' ' linkcolor: "NavyBlue"' - printf '%s\n' ' urlcolor: "NavyBlue"' - printf '%s\n' ' citecolor: "NavyBlue"' - printf '%s\n' ' table-use-row-colors: true' - printf '%s\n' ' listings-no-page-break: true' - } > .github/pandoc/pdf-defaults.yaml + cat > .github/pandoc/pdf-defaults.yaml <<'YAML' + from: markdown + standalone: true + pdf-engine: xelatex + variables: + colorlinks: true + geometry: + - margin=1in + header-left: WordPress-Security-Benchmark + header-right: Dan Knauss + footer-center: "\thepage" + titlepage: false + YAML fi - if [ ! -f .github/pandoc/epub.css ]; then { - printf '%s\n' ':root { color-scheme: light; }' - printf '%s\n' 'body { max-width: 46em; margin: 0 auto; padding: 1.2em; background: #fcfcfb; color: #111827; font-family: "Noto Serif", Georgia, serif; line-height: 1.55; }' - printf '%s\n' 'h1, h2, h3, h4 { color: #0f172a; font-family: "Noto Sans", "Helvetica Neue", Arial, sans-serif; line-height: 1.2; }' - printf '%s\n' 'h1 { font-size: 1.9em; border-bottom: 2px solid #e5e7eb; padding-bottom: 0.3em; }' - printf '%s\n' 'h2 { font-size: 1.45em; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.25em; }' - printf '%s\n' 'a { color: #0b5fff; text-decoration: none; }' - printf '%s\n' 'a:hover, a:focus { text-decoration: underline; }' - printf '%s\n' 'code, pre { font-family: "Noto Sans Mono", "SFMono-Regular", Consolas, monospace; font-size: 0.92em; }' - printf '%s\n' 'pre { padding: 0.9em; overflow-x: auto; border: 1px solid #e5e7eb; border-radius: 6px; background: #f3f4f6; }' - printf '%s\n' 'blockquote { margin: 1em 0; padding: 0.1em 1em; border-left: 4px solid #d1d5db; background: #f9fafb; color: #374151; }' - printf '%s\n' 'table { width: 100%; border-collapse: collapse; margin: 1.1em 0; font-size: 0.94em; }' - printf '%s\n' 'th, td { border: 1px solid #d1d5db; padding: 0.45em 0.6em; vertical-align: top; }' - printf '%s\n' 'th { background: #f3f4f6; font-family: "Noto Sans", "Helvetica Neue", Arial, sans-serif; }' - printf '%s\n' 'ul, ol { padding-left: 1.3em; }' + printf '%s\n' 'body { font-family: "Noto Serif", Georgia, serif; line-height: 1.6; }' + printf '%s\n' 'code, pre { font-family: "Noto Sans Mono", Menlo, monospace; }' printf '%s\n' 'img { max-width: 100%; height: auto; }' } > .github/pandoc/epub.css fi @@ -117,13 +82,9 @@ jobs: echo "md_file=WordPress-Security-Benchmark.md" >> "$GITHUB_OUTPUT" echo "base=WordPress-Security-Benchmark" >> "$GITHUB_OUTPUT" - - name: Update date in frontmatter + - name: Validate source release metadata run: | - md_file="${{ steps.inputs.outputs.md_file }}" - today=$(date '+%B %-d, %Y') - if head -1 "$md_file" | grep -q '^---$'; then - sed -i "s/^date: .*/date: \"$today\"/" "$md_file" - fi + python3 .github/scripts/validate-release-metadata.py --markdown "${{ steps.inputs.outputs.md_file }}" --changelog CHANGELOG.md - name: Convert primary document run: | @@ -131,37 +92,27 @@ jobs: base="${{ steps.inputs.outputs.base }}" docx_file="${base}.docx" - version=$(sed -n '/^---$/,/^---$/{ s/^version: *"\{0,1\}\([^"]*\)"\{0,1\}/\1/p }' "$md_file") - if [ -n "$version" ]; then - display_date="Version ${version} — $(date '+%B %-d, %Y')" + version=$(sed -n '/^---$/,/^---$/{ s/^version: *"\{0,1\}\([^"\n]*\)"\{0,1\}/\1/p }' "$md_file") + source_date=$(sed -n '/^---$/,/^---$/{ s/^date: *"\{0,1\}\([^"\n]*\)"\{0,1\}/\1/p }' "$md_file") + if [ -n "$version" ] && [ -n "$source_date" ]; then + display_date="Version $version — $source_date" + elif [ -n "$source_date" ]; then + display_date="$source_date" else - display_date="$(date '+%B %-d, %Y')" + display_date="" fi - pandoc "$md_file" \ - --reference-doc=".github/pandoc/reference.docx" \ - --toc --toc-depth=3 \ - -o "$docx_file" - - pandoc "$docx_file" \ - --from docx \ - --template eisvogel \ - --pdf-engine xelatex \ - --defaults ".github/pandoc/pdf-defaults.yaml" \ - --metadata "date=${display_date}" \ - --toc --toc-depth=3 \ - -o "${base}.pdf" - - pandoc "$docx_file" \ - --from docx \ - --css ".github/pandoc/epub.css" \ - --toc --toc-depth=3 \ - --epub-title-page=false \ - --metadata "lang=en-US" \ - -o "${base}.epub" + pandoc "$md_file" --reference-doc=".github/pandoc/reference.docx" --toc --toc-depth=3 -o "$docx_file" + + pandoc "$docx_file" --from docx --template eisvogel --pdf-engine xelatex --defaults ".github/pandoc/pdf-defaults.yaml" --metadata "date=${display_date}" --toc --toc-depth=3 -o "${base}.pdf" + + pandoc "$docx_file" --from docx --css ".github/pandoc/epub.css" --toc --toc-depth=3 --epub-title-page=false --metadata "lang=en-US" -o "${base}.epub" + + - name: Validate generated artifacts + run: python3 .github/scripts/validate-artifacts.py - name: Upload generated document bundle - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: generated-docs-bundle path: | @@ -172,67 +123,3 @@ jobs: .github/pandoc/reference.docx .github/pandoc/pdf-defaults.yaml .github/pandoc/epub.css - - validate-artifacts: - needs: build-docs - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Download generated document bundle - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c - with: - name: generated-docs-bundle - path: . - - - name: Install PDF text extractor - run: | - sudo apt-get update - sudo apt-get install -y poppler-utils - - - name: Validate generated artifacts - run: python3 .github/scripts/validate-artifacts.py - - - publish-generated-docs: - needs: validate-artifacts - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Download generated document bundle - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c - with: - name: generated-docs-bundle - path: . - - - name: Commit generated documents - run: | - branch="${GITHUB_REF_NAME:-main}" - - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config user.name "github-actions[bot]" - git add \ - "WordPress-Security-Benchmark.md" \ - "WordPress-Security-Benchmark.pdf" \ - "WordPress-Security-Benchmark.docx" \ - "WordPress-Security-Benchmark.epub" \ - ".github/pandoc/reference.docx" \ - ".github/pandoc/pdf-defaults.yaml" \ - ".github/pandoc/epub.css" \ - 2>/dev/null || true - - if ! git diff --staged --quiet; then - git commit -m "docs: regenerate PDF, Word, and EPUB documents [skip ci]" - git pull --rebase origin "$branch" - git push - else - echo "No changes to commit" - fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e5d8a1..24cb4ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,23 +4,22 @@ on: push: tags: - 'v*' - create: workflow_dispatch: inputs: tag: description: 'Tag to release, for example v1.1.0' required: true type: string + release_date: + description: 'Release date to enforce (YYYY-MM-DD). Defaults to today in UTC.' + required: false + type: string permissions: contents: write jobs: release: - if: | - github.event_name == 'workflow_dispatch' || - ( github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') ) || - ( github.event_name == 'create' && github.ref_type == 'tag' && startsWith(github.ref, 'v') ) runs-on: ubuntu-latest concurrency: group: release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name || github.ref }} @@ -28,10 +27,98 @@ jobs: env: RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name || github.ref }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }} + - name: Install Pandoc + run: | + PANDOC_VER=3.6.3 + wget -q "https://github.com/jgm/pandoc/releases/download/${PANDOC_VER}/pandoc-${PANDOC_VER}-1-amd64.deb" + sudo dpkg -i "pandoc-${PANDOC_VER}-1-amd64.deb" + rm "pandoc-${PANDOC_VER}-1-amd64.deb" + + - name: Install LaTeX, fonts, and validators + run: | + sudo apt-get update -qq + sudo apt-get install -y texlive-xetex texlive-latex-recommended texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra lmodern fonts-noto-core fonts-noto-extra epubcheck poppler-utils unzip + + - name: Install eisvogel template + run: | + mkdir -p ~/.pandoc/templates + TMPDIR_EIS=$(mktemp -d) + curl -sL "https://github.com/Wandmalfarbe/pandoc-latex-template/releases/latest/download/Eisvogel.tar.gz" | tar -xz -C "$TMPDIR_EIS" + find "$TMPDIR_EIS" -name "*.latex" -exec cp {} ~/.pandoc/templates/ \; + rm -rf "$TMPDIR_EIS" + + - name: Bootstrap pandoc styling assets + run: | + mkdir -p .github/pandoc + if [ ! -f .github/pandoc/reference.docx ]; then + pandoc --print-default-data-file reference.docx > .github/pandoc/reference.docx + fi + if [ ! -f .github/pandoc/pdf-defaults.yaml ]; then + cat > .github/pandoc/pdf-defaults.yaml <<'YAML' + from: markdown + standalone: true + pdf-engine: xelatex + variables: + colorlinks: true + geometry: + - margin=1in + header-left: WordPress-Security-Benchmark + header-right: Dan Knauss + footer-center: "\thepage" + titlepage: false + YAML + fi + if [ ! -f .github/pandoc/epub.css ]; then + { + printf '%s\n' 'body { font-family: "Noto Serif", Georgia, serif; line-height: 1.6; }' + printf '%s\n' 'code, pre { font-family: "Noto Sans Mono", Menlo, monospace; }' + printf '%s\n' 'img { max-width: 100%; height: auto; }' + } > .github/pandoc/epub.css + fi + + - name: Resolve release date + id: release_meta + run: | + release_date="${{ inputs.release_date }}" + if [ -z "$release_date" ]; then + release_date=$(date -u '+%Y-%m-%d') + fi + echo "release_date=$release_date" >> "$GITHUB_OUTPUT" + + - name: Validate release metadata + run: | + python3 .github/scripts/validate-release-metadata.py --markdown "WordPress-Security-Benchmark.md" --changelog CHANGELOG.md --expected-tag "$RELEASE_TAG" --expected-date "${{ steps.release_meta.outputs.release_date }}" --strict-changelog + + - name: Convert primary document + run: | + md_file="WordPress-Security-Benchmark.md" + base="WordPress-Security-Benchmark" + docx_file="${base}.docx" + + version=$(sed -n '/^---$/,/^---$/{ s/^version: *"\{0,1\}\([^"\n]*\)"\{0,1\}/\1/p }' "$md_file") + source_date=$(sed -n '/^---$/,/^---$/{ s/^date: *"\{0,1\}\([^"\n]*\)"\{0,1\}/\1/p }' "$md_file") + if [ -n "$version" ] && [ -n "$source_date" ]; then + display_date="Version $version — $source_date" + elif [ -n "$source_date" ]; then + display_date="$source_date" + else + display_date="" + fi + + pandoc "$md_file" --reference-doc=".github/pandoc/reference.docx" --toc --toc-depth=3 -o "$docx_file" + + pandoc "$docx_file" --from docx --template eisvogel --pdf-engine xelatex --defaults ".github/pandoc/pdf-defaults.yaml" --metadata "date=${display_date}" --toc --toc-depth=3 -o "${base}.pdf" + + pandoc "$docx_file" --from docx --css ".github/pandoc/epub.css" --toc --toc-depth=3 --epub-title-page=false --metadata "lang=en-US" -o "${base}.epub" + + - name: Validate generated artifacts + run: python3 .github/scripts/validate-artifacts.py + - name: Create GitHub Release uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda with: diff --git a/.github/workflows/validate-artifacts.yml b/.github/workflows/validate-artifacts.yml index f2f27c7..6e85309 100644 --- a/.github/workflows/validate-artifacts.yml +++ b/.github/workflows/validate-artifacts.yml @@ -3,9 +3,14 @@ name: Validate Artifacts on: push: branches: [main] - paths: + paths: &artifact_paths + - 'WordPress-Security-Benchmark.pdf' + - 'WordPress-Security-Benchmark.docx' + - 'WordPress-Security-Benchmark.epub' - '.github/scripts/validate-artifacts.py' - '.github/workflows/validate-artifacts.yml' + pull_request: + paths: *artifact_paths workflow_dispatch: permissions: diff --git a/.github/workflows/validate-pdf-visual.yml b/.github/workflows/validate-pdf-visual.yml index 377f0eb..0aadf4c 100644 --- a/.github/workflows/validate-pdf-visual.yml +++ b/.github/workflows/validate-pdf-visual.yml @@ -5,8 +5,6 @@ on: paths: - '.github/pandoc/**' - '.github/scripts/validate-pdf-visual.mjs' - - '.github/workflows/generate-docs.yml' - - '.github/workflows/validate-pdf-visual.yml' - '.github/test-artifacts/pdf-baselines/**' - 'package.json' - 'package-lock.json' @@ -15,8 +13,6 @@ on: paths: - '.github/pandoc/**' - '.github/scripts/validate-pdf-visual.mjs' - - '.github/workflows/generate-docs.yml' - - '.github/workflows/validate-pdf-visual.yml' - '.github/test-artifacts/pdf-baselines/**' - 'package.json' - 'package-lock.json' diff --git a/.github/workflows/validate-release-metadata.yml b/.github/workflows/validate-release-metadata.yml new file mode 100644 index 0000000..866124d --- /dev/null +++ b/.github/workflows/validate-release-metadata.yml @@ -0,0 +1,27 @@ +name: Validate Release Metadata + +on: + push: + branches: [main] + paths: &release_metadata_paths + - 'WordPress-Security-Benchmark.md' + - 'CHANGELOG.md' + - '.github/scripts/validate-release-metadata.py' + - '.github/workflows/validate-release-metadata.yml' + pull_request: + paths: *release_metadata_paths + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate-release-metadata: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Validate release metadata + run: | + python3 .github/scripts/validate-release-metadata.py --markdown 'WordPress-Security-Benchmark.md' --changelog CHANGELOG.md diff --git a/.planning/STATE.md b/.planning/STATE.md index 1525e1c..f5d85c9 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,59 +2,46 @@ ## Project Reference -See: .planning/PROJECT.md (updated 2026-03-21) +See: `.planning/PROJECT.md` (historical project charter) and `README.md` (current public workflow). -**Core value:** The benchmark must remain source-grounded, auditable, and safe to apply to current supported WordPress environments. -**Current focus:** Phase 1: Governance Baseline +**Core value:** Keep the benchmark source-grounded, auditable, and safe to apply to current supported WordPress environments. +**Current focus:** Maintenance backlog. The benchmark is shipped on `main`; CI, artifact generation, and series-alignment follow-up now happen incrementally rather than through the original bootstrap plan scaffold. ## Current Position -**Current Phase:** 1 -**Current Phase Name:** Governance Baseline -**Total Phases:** 4 -**Current Plan:** 0 -**Total Plans in Phase:** 2 -**Status:** Ready to plan -**Last Activity:** 2026-03-21 - Initialized GSD planning state and repaired metrics-validator parity. +**Current Phase:** Archived planning scaffold +**Current Phase Name:** Historical bootstrap retained for reference only +**Total Phases:** 4 archived scaffold directories +**Current Plan:** None +**Total Plans in Phase:** 0 active +**Status:** Maintenance mode; no active phased plan +**Last Activity:** 2026-06-17 — refreshed artifact-validation strategy and marked the original `.planning/` scaffold as historical so it no longer reads like pending work. -**Progress:** 0% [░░░░░░░░░░] +**Progress:** 100% of the original bootstrap scope is complete; further work is backlog-driven. -## Performance Metrics +## Context -**Velocity:** -- Total plans completed: 0 -- Average duration: 0 min -- Total execution time: 0.0 hours - -**By Phase:** - -| Phase | Plans | Total | Avg/Plan | -|-------|-------|-------|----------| -| - | - | - | - | - -**Recent Trend:** -- Last 5 plans: none yet -- Trend: Stable - -*GSD tracking begins with this milestone; historical v1.0 work was not reconstructed into plan metrics.* +- The canonical benchmark is published and maintained from `main`. +- Metrics, generated artifacts, release automation, and visual validation workflows already exist. +- The `.planning/phases/01-*` through `04-*` directories are retained only as placeholders from the initial GSD bootstrap, not as active work queues. ## Decisions Made -| Phase | Summary | Rationale | -|-------|---------|-----------| -| Init | Start GSD tracking from the next maintenance milestone. | The shipped benchmark predates `.planning/`, so reconstructing historical execution would add mostly invented detail. | -| Init | Treat the repository as a documentation product with validation-heavy defaults. | Benchmark guidance is high-leverage security documentation, so drift and unverifiable edits are more dangerous than slower planning. | +| Date | Summary | Rationale | +|---|---|---| +| 2026-03-21 | Start repo-local GSD tracking from the next maintenance milestone rather than reconstructing v1.0 history. | Backfilling fictional execution detail would have reduced trust in the planning record. | +| 2026-06-17 | Reclassify the original phase scaffold as historical reference only. | The repo has already shipped the work those placeholders were meant to organize, so leaving them framed as active plans is misleading. | ## Pending Todos -None yet. +- Track future benchmark changes through explicit backlog or release-ready plan files only when new scoped work is approved. ## Blockers -None yet. +None. ## Session -**Last Date:** 2026-03-21 16:43 MDT -**Stopped At:** Project initialization complete; Phase 1 is ready for discussion and planning. +**Last Date:** 2026-06-17 MDT +**Stopped At:** Historical planning scaffold normalized; benchmark remains in maintenance mode. **Resume File:** None diff --git a/CHANGELOG.md b/CHANGELOG.md index ea98cef..1c1f0f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to the WordPress Security Benchmark. ## Unreleased ### Added +- Added release-metadata validation so frontmatter version/date and the latest changelog release heading stay aligned, with optional tag/date enforcement during release publication. - Added a `Series review` issue form so quarterly and pre-release cross-document alignment checks can be tracked explicitly. - Added a repo-local generated-artifact smoke validator and a dedicated `Validate Artifacts` workflow for PDF, EPUB, and DOCX outputs. - Added a Playwright-based PDF visual smoke test and dedicated workflow with committed baselines for critical page regions. @@ -12,6 +13,8 @@ All notable changes to the WordPress Security Benchmark. - Added Learn WordPress's [Writing in the WordPress voice](https://learn.wordpress.org/course/writing-in-the-wordpress-voice/) as the recommended WordPress-specific voice and accessibility reference when benchmark findings are adapted into stakeholder communications. ### Changed +- Moved full PDF/DOCX/EPUB publication to the tag-driven release workflow and converted `generate-docs.yml` into a manual preview/build workflow instead of an automatic `main`-push publisher. +- Made the generated-artifact validator read the expected version string from the Markdown frontmatter instead of hardcoding `Version 1.1`, preventing future publish-flow failures after routine version bumps. - Separated Playwright PDF visual validation from the artifact publish path so `generate-docs.yml` can publish after artifact checks while the dedicated visual workflow handles layout regression checks on workflow, packaging, and Pandoc changes. - Corrected current-version framing to reflect the public WordPress 7.0 release and remove stale pre-release scheduling language. - Tightened AI secret-management guidance for WordPress 7.0 by adding Connectors API credential-source and database-storage context to control 11.1.