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
152 changes: 152 additions & 0 deletions scripts/check_generated_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@
from _ci_utils import display_path, read_bytes, repo_path, repo_root, write_bytes

EXPORT_SCRIPT = Path("scripts/export_requirements.py")
SYNC_INDEX_SCRIPT = Path("scripts/sync_index_from_readme.py")
SYNC_TAB_ACKNOWLEDGEMENTS_SCRIPT = Path("scripts/sync_tab_acknowledgements_from_acknowledgements.py")
REQUIREMENTS_JSON = Path("standard/apts_requirements.json")
SCHEMA_JSON = Path("standard/apts_requirements_schema.json")
README_MD = Path("README.md")
INDEX_MD = Path("index.md")
ACKNOWLEDGEMENTS_MD = Path("ACKNOWLEDGEMENTS.md")
TAB_ACKNOWLEDGEMENTS_MD = Path("tab_acknowledgements.md")
FIXED_TIMESTAMP = "1970-01-01T00:00:00Z"


Expand Down Expand Up @@ -129,6 +135,152 @@ def main() -> int:
print(message)
exit_code = 1

index_check_exit_code = check_index_in_sync_with_readme()
if index_check_exit_code != 0:
exit_code = index_check_exit_code

tab_check_exit_code = check_tab_acknowledgements_in_sync()
if tab_check_exit_code != 0:
exit_code = tab_check_exit_code

return exit_code


def check_tab_acknowledgements_in_sync() -> int:
"""Verify tab_acknowledgements.md matches the ACKNOWLEDGEMENTS.md body
after the H1 and the "Influences" / "How to Get Listed" sections are
stripped.

The sync_tab_acknowledgements_from_acknowledgements.py script regenerates
tab_acknowledgements.md from ACKNOWLEDGEMENTS.md. This check fails the
build if the two have drifted.
"""

if not repo_path(SYNC_TAB_ACKNOWLEDGEMENTS_SCRIPT).is_file():
print(
f"No sync script found at {display_path(SYNC_TAB_ACKNOWLEDGEMENTS_SCRIPT)}. "
"Skipping tab_acknowledgements.md / ACKNOWLEDGEMENTS.md sync check."
)
return 0

if (
not repo_path(TAB_ACKNOWLEDGEMENTS_MD).is_file()
or not repo_path(ACKNOWLEDGEMENTS_MD).is_file()
):
print(
"FAILED: Expected ACKNOWLEDGEMENTS.md and tab_acknowledgements.md "
"to both exist for the sync check."
)
return 1

original_tab_contents = read_bytes(TAB_ACKNOWLEDGEMENTS_MD)
exit_code = 0

try:
subprocess.run(
[sys.executable, str(repo_path(SYNC_TAB_ACKNOWLEDGEMENTS_SCRIPT))],
cwd=repo_root(),
check=True,
)

regenerated_tab = read_bytes(TAB_ACKNOWLEDGEMENTS_MD)
expected_lines = raw_text_lines(original_tab_contents)
actual_lines = raw_text_lines(regenerated_tab)

if expected_lines != actual_lines:
print_diff(
"FAILED: tab_acknowledgements.md is out of sync with "
"ACKNOWLEDGEMENTS.md. Run "
"scripts/sync_tab_acknowledgements_from_acknowledgements.py to regenerate it.",
expected_lines=expected_lines,
actual_lines=actual_lines,
expected_name="committed tab_acknowledgements.md",
actual_name="regenerated tab_acknowledgements.md",
)
exit_code = 1
except subprocess.CalledProcessError as exc:
print(
f"FAILED: sync_tab_acknowledgements_from_acknowledgements.py exited with status {exc.returncode}."
)
exit_code = 1
except Exception as exc:
print(
f"FAILED: tab_acknowledgements.md / ACKNOWLEDGEMENTS.md sync check could not complete: {exc}"
)
exit_code = 1
finally:
restore_errors = restore_original_files(
{TAB_ACKNOWLEDGEMENTS_MD: original_tab_contents}
)
if restore_errors:
for message in restore_errors:
print(message)
exit_code = 1

return exit_code


def check_index_in_sync_with_readme() -> int:
"""Verify index.md matches the README.md body under the Jekyll front matter.

README.md is the canonical source for the project overview; index.md
is the Jekyll-rendered version with a YAML front matter block prepended.
The sync_index_from_readme.py script regenerates index.md from README.md.
This check fails the build if the two have drifted.
"""

if not repo_path(SYNC_INDEX_SCRIPT).is_file():
print(
f"No sync script found at {display_path(SYNC_INDEX_SCRIPT)}. "
"Skipping index.md / README.md sync check."
)
return 0

if not repo_path(INDEX_MD).is_file() or not repo_path(README_MD).is_file():
print(
"FAILED: Expected README.md and index.md to both exist for the sync check."
)
return 1

original_index_contents = read_bytes(INDEX_MD)
exit_code = 0

try:
subprocess.run(
[sys.executable, str(repo_path(SYNC_INDEX_SCRIPT))],
cwd=repo_root(),
check=True,
)

regenerated_index = read_bytes(INDEX_MD)
expected_lines = raw_text_lines(original_index_contents)
actual_lines = raw_text_lines(regenerated_index)

if expected_lines != actual_lines:
print_diff(
"FAILED: index.md is out of sync with README.md. "
"Run scripts/sync_index_from_readme.py to regenerate it.",
expected_lines=expected_lines,
actual_lines=actual_lines,
expected_name="committed index.md",
actual_name="regenerated index.md",
)
exit_code = 1
except subprocess.CalledProcessError as exc:
print(
f"FAILED: sync_index_from_readme.py exited with status {exc.returncode}."
)
exit_code = 1
except Exception as exc:
print(f"FAILED: index.md / README.md sync check could not complete: {exc}")
exit_code = 1
finally:
restore_errors = restore_original_files({INDEX_MD: original_index_contents})
if restore_errors:
for message in restore_errors:
print(message)
exit_code = 1

return exit_code


Expand Down
107 changes: 107 additions & 0 deletions scripts/sync_index_from_readme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Regenerate index.md from README.md while preserving the Jekyll front matter.

README.md is the canonical source for the project overview shown on both the
GitHub repo home and the OWASP project page (owasp.org/APTS/). index.md is the
Jekyll-rendered version of the same content with a YAML front matter block at
the top.

This script keeps the two in sync: it copies the body of README.md into
index.md, leaving the existing YAML front matter at the top of index.md
untouched.

Run this whenever README.md changes. The CI artifact check
(scripts/check_generated_artifacts.py) verifies the two stay in sync and
fails the build if they drift.

Usage:
python scripts/sync_index_from_readme.py
"""

from __future__ import annotations

from pathlib import Path

from _ci_utils import display_path, read_text, repo_path

README = Path("README.md")
INDEX = Path("index.md")
FRONT_MATTER_DELIMITER = "---"


def split_front_matter(text: str) -> tuple[str, str]:
"""Return (front_matter_block, body) for a Jekyll markdown file.

The front matter block includes the leading and trailing `---` lines and
any whitespace immediately following the closing delimiter so that the
returned body starts cleanly. If no front matter is present, an empty
front matter block is returned and the entire text is treated as body.
"""

lines = text.splitlines(keepends=True)
if not lines or lines[0].rstrip("\r\n") != FRONT_MATTER_DELIMITER:
return "", text

closing_index = None
for index in range(1, len(lines)):
if lines[index].rstrip("\r\n") == FRONT_MATTER_DELIMITER:
closing_index = index
break

if closing_index is None:
# Malformed front matter (no closing delimiter). Bail out and treat
# the file as bodyless rather than silently corrupting it.
raise ValueError(
f"Malformed Jekyll front matter in index.md: no closing '{FRONT_MATTER_DELIMITER}' "
"delimiter found."
)

front_matter = "".join(lines[: closing_index + 1])

# Skip a single blank line directly after the closing delimiter so the
# body we return doesn't start with a stray newline. Anything beyond that
# is body content.
body_start = closing_index + 1
if body_start < len(lines) and lines[body_start].strip() == "":
body_start += 1

body = "".join(lines[body_start:])
return front_matter, body


def build_index(front_matter: str, readme_text: str) -> str:
"""Compose index.md from the existing front matter and the README body."""

if not front_matter:
raise ValueError(
f"{display_path(INDEX)} is missing a Jekyll front matter block. "
"Restore the front matter manually before running the sync."
)

return f"{front_matter}\n{readme_text}"


def main() -> int:
if not repo_path(README).is_file():
print(f"FAILED: {display_path(README)} not found.")
return 1
if not repo_path(INDEX).is_file():
print(f"FAILED: {display_path(INDEX)} not found.")
return 1

readme_text = read_text(README)
index_text = read_text(INDEX)

front_matter, _existing_body = split_front_matter(index_text)
new_index_text = build_index(front_matter, readme_text)

if new_index_text == index_text:
print(f"{display_path(INDEX)} already in sync with {display_path(README)}.")
return 0

repo_path(INDEX).write_text(new_index_text, encoding="utf-8")
print(f"Updated {display_path(INDEX)} from {display_path(README)}.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading