Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
88aee37
Docs versioning: Add glob support, better validation and reporting
C-Achard Apr 9, 2026
7d43887
Add metadata sync check and CI helpers
C-Achard Apr 9, 2026
9830c11
Add metadata sync suggestions to report
C-Achard Apr 9, 2026
4a6914a
Limit docs/notebooks scan to changed files
C-Achard Apr 9, 2026
b906e9a
Handle invalid target specs in validation
C-Achard Apr 10, 2026
45175d2
tests: add repo/cfg fixtures and refactor tests
C-Achard Apr 10, 2026
17ed847
Drop .markdown from docs CI
C-Achard Apr 10, 2026
970f3e0
Fix const import in tests
C-Achard Apr 10, 2026
b017493
Handle missing timestamps and shell-quote paths
C-Achard Apr 10, 2026
c5ee232
Clarify --targets help text
C-Achard Apr 10, 2026
f5d078b
Merge branch 'cy/docs-versioning-tweaks' into cy/docs-ci-tweaks
C-Achard Apr 10, 2026
44a671d
Update tools/docs_and_notebooks_check.py
C-Achard Apr 10, 2026
6d3b1be
Update docs_and_notebooks_check.py
C-Achard Apr 10, 2026
46fdb04
Merge branch 'cy/docs-versioning-tweaks' of https://github.com/DeepLa…
C-Achard Apr 10, 2026
d9b9a1e
Update tools/docs_and_notebooks_check.py
C-Achard Apr 10, 2026
8667245
Update .github/workflows/docs_and_notebooks_checks.yml
C-Achard Apr 10, 2026
f586380
Quote config path
C-Achard Apr 10, 2026
112dac7
Revert "Merge branch 'cy/docs-versioning-tweaks' of https://github.co…
C-Achard Apr 10, 2026
10ddf01
Use TypedDict for targets and simplify glob matching
C-Achard Apr 10, 2026
5f869f9
Merge branch 'cy/docs-versioning-tweaks' into cy/docs-ci-tweaks
C-Achard Apr 10, 2026
c9a8939
Avoid mixing metadata sync needed and invalid meta
C-Achard Apr 20, 2026
fbfa748
Add tests for metadata sync warnings/enforcement
C-Achard Apr 20, 2026
cd27fcf
Merge branch 'main' into cy/docs-ci-tweaks
C-Achard Apr 20, 2026
c3543d4
Avoid metadata sync on parse errors
C-Achard May 5, 2026
a118544
Change schema version type to Literal
C-Achard May 5, 2026
854647a
Merge branch 'main' into cy/docs-ci-tweaks
C-Achard May 5, 2026
56fec67
Fix mismatched test error message
C-Achard May 5, 2026
16e3f95
Case insensitive check for file ext
C-Achard May 11, 2026
2a85985
Merge branch 'main' into cy/docs-ci-tweaks
C-Achard May 12, 2026
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
65 changes: 55 additions & 10 deletions .github/workflows/docs_and_notebooks_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ permissions:

jobs:
staleness:
name: Docs and notebooks scan (read-only)
name: Docs and notebooks scan (changed docs only)
runs-on: ubuntu-latest
timeout-minutes: 5

Comment thread
C-Achard marked this conversation as resolved.
Expand All @@ -31,29 +31,74 @@ jobs:
python -m pip install --upgrade pip
python -m pip install "pydantic>=2,<3" pyyaml "nbformat>=5"

- name: Collect changed .md/.ipynb files
id: changed_docs
shell: bash
run: |
set -euo pipefail
mkdir -p tmp/docs_nb_checks

if [[ "${{ github.event_name }}" == "pull_request" ]]; then
base="${{ github.event.pull_request.base.sha }}"
head="${{ github.event.pull_request.head.sha }}"
else
base="${{ github.event.before }}"
head="${{ github.sha }}"
fi

git diff --name-only --diff-filter=ACMR "$base" "$head" \
| { grep -iE '\.(md|ipynb)$' || true; } \
| sort -u > tmp/docs_nb_checks/changed_docs.txt

Comment thread
C-Achard marked this conversation as resolved.
count=$(wc -l < tmp/docs_nb_checks/changed_docs.txt | tr -d ' ')
echo "count=$count" >> "$GITHUB_OUTPUT"

echo "Changed docs files:"
if [[ "$count" -eq 0 ]]; then
echo "(none)"
else
sed 's/^/- /' tmp/docs_nb_checks/changed_docs.txt
fi

- name: Run staleness report (read-only)
if: steps.changed_docs.outputs.count != '0'
shell: bash
run: |
python tools/docs_and_notebooks_check.py \
--config tools/docs_and_notebooks_report_config.yml \
--out-dir tmp/docs_nb_checks \
report
set -euo pipefail
mapfile -t targets < tmp/docs_nb_checks/changed_docs.txt

python tools/docs_and_notebooks_check.py \
--config tools/docs_and_notebooks_report_config.yml \
--out-dir tmp/docs_nb_checks \
report \
--targets "${targets[@]}"

# Optional: run check mode (will fail only once you populate allowlists in config)
- name: Run staleness policy check (optional gate)
if: steps.changed_docs.outputs.count != '0'
continue-on-error: true
shell: bash
run: |
set -euo pipefail
mapfile -t targets < tmp/docs_nb_checks/changed_docs.txt

python tools/docs_and_notebooks_check.py \
--config tools/docs_and_notebooks_report_config.yml \
--out-dir tmp/docs_nb_checks \
--no-step-summary \
check
--config tools/docs_and_notebooks_report_config.yml \
--out-dir tmp/docs_nb_checks \
--no-step-summary \
check \
--targets "${targets[@]}"

- name: No changed docs to scan
if: steps.changed_docs.outputs.count == '0'
run: echo "No changed .md or .ipynb files found in the repo. Skipping scan."

- name: Upload staleness artifacts
if: steps.changed_docs.outputs.count != '0'
uses: actions/upload-artifact@v4
with:
name: staleness-report
path: |
tmp/docs_nb_checks/*.json
tmp/docs_nb_checks/*.md
tmp/docs_nb_checks/changed_docs.txt
if-no-files-found: error
117 changes: 116 additions & 1 deletion tests/tools/docs_and_notebooks_checks/test_check_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ def test_write_outputs_contract(tool, repo: Path, cfg, tmp_path: Path):
assert md_path.exists()

payload = json.loads(json_path.read_text(encoding="utf-8"))
assert payload["schema_version"] == tool.SCHEMA_VERSION
assert payload["schema_version"] == tool.REPORT_SCHEMA_VERSION
assert "records" in payload and isinstance(payload["records"], list)
assert md_path.read_text(encoding="utf-8").startswith("#")

Expand Down Expand Up @@ -550,3 +550,118 @@ def test_main_returns_2_for_invalid_target_selector(tool, repo: Path, monkeypatc

rc = tool.main(["--config", str(cfg_path), "report", "--targets", "./"])
assert rc == 2


@pytest.mark.parametrize(
("rel", "kind"),
[
("docs/page.md", "md"),
("docs/nbs/nb.ipynb", "ipynb"),
],
)
def test_metadata_sync_warning_populates_report_targets_and_command(tool, repo: Path, cfg, rel: str, kind: str):
"""
Out-of-sync embedded last_content_updated should:
- add metadata_sync_needed warning
- appear in Report.metadata_sync_targets
- generate a non-empty metadata_sync_command
"""
embedded_date = "2000-01-01"

if kind == "md":
_write(
repo,
rel,
f"---\ndeeplabcut:\n last_content_updated: {embedded_date}\n---\n# hello\n",
)
else:
nb = tool.nbformat.v4.new_notebook(metadata={tool.DLC_NAMESPACE: {"last_content_updated": embedded_date}})
_write(
repo,
rel,
tool.nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + "\n",
)

# Git-derived content date differs from embedded metadata date
_git_commit(repo, "docs: add file", "2020-01-01T12:00:00+00:00")

tool_cfg = cfg(include=[rel])
records = tool.scan_files(repo, tool_cfg, targets=[rel])

assert len(records) == 1
rec = records[0]

# Sanity check: embedded metadata is parsed, but differs from git-derived date
assert rec.meta is not None
assert rec.meta.last_content_updated == date(2000, 1, 1)
assert rec.last_content_updated == date(2020, 1, 1)

# New warning should be present
assert "metadata_sync_needed" in rec.warnings

metadata_sync_targets = tool.collect_metadata_sync_targets(records)
metadata_sync_command = tool.build_metadata_sync_command(
"tools/docs_and_notebooks_report_config.yml",
metadata_sync_targets,
)

report = tool.Report(
generated_at=datetime.now(timezone.utc),
repo_root=str(repo),
config_path="tools/docs_and_notebooks_report_config.yml",
totals=tool.summarize(records),
records=records,
metadata_sync_targets=metadata_sync_targets,
metadata_sync_command=metadata_sync_command,
)

assert report.metadata_sync_targets == [rel]
assert report.metadata_sync_command
assert "--set-content-date-from-git" in report.metadata_sync_command
assert "--targets" in report.metadata_sync_command
assert rel in report.metadata_sync_command

# Optional: also verify the markdown report renders the guidance section
rendered = tool.to_markdown(report, tool_cfg)
assert "## Metadata sync suggestions" in rendered
assert f"- `{rel}`" in rendered


@pytest.mark.parametrize(
("rel", "kind"),
[
("docs/page.md", "md"),
("docs/nbs/nb.ipynb", "ipynb"),
],
)
def test_enforce_fails_when_metadata_sync_needed_is_configured(tool, repo: Path, cfg, rel: str, kind: str):
"""
If policy.fail_if_metadata_sync_needed=True, an out-of-sync file should
become a policy violation in check/enforcement mode.
"""
embedded_date = "2000-01-01"

if kind == "md":
_write(
repo,
rel,
f"---\ndeeplabcut:\n last_content_updated: {embedded_date}\n---\n# hello\n",
)
else:
nb = tool.nbformat.v4.new_notebook(metadata={tool.DLC_NAMESPACE: {"last_content_updated": embedded_date}})
_write(
repo,
rel,
tool.nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + "\n",
)

_git_commit(repo, "docs: add file", "2020-01-01T12:00:00+00:00")

tool_cfg = cfg(include=[rel], fail_if_metadata_sync_needed=True)
records = tool.scan_files(repo, tool_cfg, targets=[rel])

violations = tool.enforce(tool_cfg, records)

assert violations == [
f"{rel}: embedded last_content_updated is missing or out of sync with git content update date"
]
Loading
Loading