Skip to content

Commit a5db42a

Browse files
does this work?
1 parent a08a604 commit a5db42a

File tree

6 files changed

+285
-0
lines changed

6 files changed

+285
-0
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
#!/usr/bin/env python3
2+
3+
import datetime
4+
import os
5+
import pathlib
6+
import subprocess
7+
from collections import defaultdict
8+
from typing import List, Optional, Tuple
9+
10+
WINDOW_DAYS = int(os.getenv("WINDOW_DAYS", "30"))
11+
SIGNIFICANT_LINE_THRESHOLD = int(os.getenv("SIGNIFICANT_LINE_THRESHOLD", "20"))
12+
CLASS_NAME = "sidebar-item--updated"
13+
TARGET_DIRS = ("user-guide", "developer-guide")
14+
TARGET_EXTS = {".md", ".mdx"}
15+
EXCLUDED_PATH_PREFIXES = ("user-guide/01-getting-started/01-assembly_guides/",)
16+
17+
18+
def run_git(args: List[str]) -> str:
19+
result = subprocess.run(
20+
["git", *args],
21+
check=True,
22+
capture_output=True,
23+
text=True,
24+
)
25+
return result.stdout
26+
27+
28+
def list_tracked_docs() -> List[str]:
29+
output = run_git(["ls-files", *TARGET_DIRS])
30+
paths = []
31+
for line in output.splitlines():
32+
path = line.strip()
33+
if path.startswith(EXCLUDED_PATH_PREFIXES):
34+
continue
35+
if pathlib.Path(path).suffix in TARGET_EXTS:
36+
paths.append(path)
37+
return paths
38+
39+
40+
def since_date(days: int) -> str:
41+
return (datetime.datetime.utcnow() - datetime.timedelta(days=days)).strftime(
42+
"%Y-%m-%d"
43+
)
44+
45+
46+
def files_created_since(since: str, doc_set: set[str]) -> set[str]:
47+
output = run_git(
48+
[
49+
"log",
50+
"--since",
51+
since,
52+
"--diff-filter=A",
53+
"--name-only",
54+
"--pretty=format:",
55+
"--",
56+
*TARGET_DIRS,
57+
]
58+
)
59+
return {line.strip() for line in output.splitlines() if line.strip() in doc_set}
60+
61+
62+
def change_counts_since(since: str, doc_set: set[str]) -> dict[str, int]:
63+
output = run_git(
64+
[
65+
"log",
66+
"--since",
67+
since,
68+
"--pretty=format:",
69+
"--numstat",
70+
"--",
71+
*TARGET_DIRS,
72+
]
73+
)
74+
counts: defaultdict[str, int] = defaultdict(int)
75+
for line in output.splitlines():
76+
parts = line.split("\t")
77+
if len(parts) != 3:
78+
continue
79+
added, removed, path = parts
80+
path = path.strip()
81+
if path not in doc_set:
82+
continue
83+
if added == "-" or removed == "-":
84+
counts[path] = max(counts[path], SIGNIFICANT_LINE_THRESHOLD)
85+
continue
86+
try:
87+
counts[path] += int(added) + int(removed)
88+
except ValueError:
89+
continue
90+
return counts
91+
92+
93+
def split_front_matter(
94+
text: str,
95+
) -> Tuple[Optional[List[str]], List[str]]:
96+
lines = text.splitlines()
97+
if lines and lines[0].strip() == "---":
98+
for idx in range(1, len(lines)):
99+
if lines[idx].strip() == "---":
100+
return lines[1:idx], lines[idx + 1 :]
101+
return None, lines
102+
103+
104+
def find_sidebar_line(front_lines: List[str]) -> Tuple[Optional[int], Optional[str]]:
105+
for idx, line in enumerate(front_lines):
106+
if line.lstrip().startswith("sidebar_class_name:"):
107+
return idx, line
108+
return None, None
109+
110+
111+
def parse_sidebar_value(line: str) -> str:
112+
value = line.split(":", 1)[1].strip()
113+
if value.startswith(("'", '"')) and value.endswith(("'", '"')):
114+
return value[1:-1]
115+
return value
116+
117+
118+
def format_sidebar_line(original_line: str, value: str) -> str:
119+
prefix = original_line.split("sidebar_class_name:")[0]
120+
return f"{prefix}sidebar_class_name: {value}"
121+
122+
123+
def apply_sidebar_flag(
124+
front_lines: Optional[List[str]],
125+
body_lines: List[str],
126+
should_have_flag: bool,
127+
) -> Tuple[Optional[List[str]], List[str], bool]:
128+
if front_lines is None:
129+
if not should_have_flag:
130+
return None, body_lines, False
131+
new_body = list(body_lines)
132+
if new_body and new_body[0].strip():
133+
new_body.insert(0, "")
134+
return [f"sidebar_class_name: {CLASS_NAME}"], new_body, True
135+
136+
front = list(front_lines)
137+
sidebar_idx, sidebar_line = find_sidebar_line(front)
138+
changed = False
139+
140+
if should_have_flag:
141+
if sidebar_idx is None:
142+
front.append(f"sidebar_class_name: {CLASS_NAME}")
143+
changed = True
144+
else:
145+
current_value = parse_sidebar_value(sidebar_line)
146+
tokens = current_value.split()
147+
if CLASS_NAME not in tokens:
148+
tokens.append(CLASS_NAME)
149+
updated_value = " ".join(tokens)
150+
front[sidebar_idx] = format_sidebar_line(sidebar_line, updated_value)
151+
changed = True
152+
else:
153+
if sidebar_idx is None:
154+
return front, body_lines, False
155+
current_value = parse_sidebar_value(sidebar_line)
156+
tokens = [token for token in current_value.split() if token != CLASS_NAME]
157+
if tokens:
158+
updated_value = " ".join(tokens)
159+
if updated_value != current_value:
160+
front[sidebar_idx] = format_sidebar_line(sidebar_line, updated_value)
161+
changed = True
162+
else:
163+
del front[sidebar_idx]
164+
while front and not front[-1].strip():
165+
front.pop()
166+
if not any(line.strip() for line in front):
167+
front = None
168+
changed = True
169+
170+
return front, body_lines, changed
171+
172+
173+
def assemble(
174+
front_lines: Optional[List[str]], body_lines: List[str], end_with_newline: bool
175+
) -> str:
176+
parts: List[str] = []
177+
if front_lines is not None:
178+
parts.append("---")
179+
parts.extend(front_lines)
180+
parts.append("---")
181+
parts.extend(body_lines)
182+
text = "\n".join(parts)
183+
if end_with_newline and not text.endswith("\n"):
184+
text += "\n"
185+
return text
186+
187+
188+
def update_file(path: pathlib.Path, should_have_flag: bool) -> bool:
189+
original_text = path.read_text(encoding="utf-8")
190+
keep_trailing_newline = original_text.endswith("\n")
191+
front_lines, body_lines = split_front_matter(original_text)
192+
new_front, new_body, changed = apply_sidebar_flag(
193+
front_lines, body_lines, should_have_flag
194+
)
195+
if not changed:
196+
return False
197+
new_text = assemble(new_front, new_body, keep_trailing_newline)
198+
path.write_text(new_text, encoding="utf-8")
199+
return True
200+
201+
202+
def main() -> None:
203+
docs = list_tracked_docs()
204+
doc_set = set(docs)
205+
since = since_date(WINDOW_DAYS)
206+
207+
created_recently = files_created_since(since, doc_set)
208+
change_counts = change_counts_since(since, doc_set)
209+
significant_changes = {
210+
path for path, total in change_counts.items() if total >= SIGNIFICANT_LINE_THRESHOLD
211+
}
212+
213+
to_mark = created_recently | significant_changes
214+
215+
changed_files = []
216+
for path_str in sorted(doc_set):
217+
path = pathlib.Path(path_str)
218+
should_have_flag = path_str in to_mark
219+
if update_file(path, should_have_flag):
220+
changed_files.append(path_str)
221+
222+
print(f"Window start: {since}")
223+
print(f"New files in window: {len(created_recently)}")
224+
print(f"Files with significant changes: {len(significant_changes)}")
225+
if changed_files:
226+
print("Updated sidebar_class_name in:")
227+
for path in changed_files:
228+
print(f" - {path}")
229+
else:
230+
print("No files required updates.")
231+
232+
233+
if __name__ == "__main__":
234+
main()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Flag updated docs
2+
3+
on:
4+
schedule:
5+
- cron: "30 3 * * *"
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
mark-updated-docs:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: "3.x"
24+
25+
- name: Mark recently updated docs
26+
env:
27+
SIGNIFICANT_LINE_THRESHOLD: "20"
28+
WINDOW_DAYS: "30"
29+
run: python .github/scripts/mark_recent_docs.py
30+
31+
- name: Detect changes
32+
id: changes
33+
run: |
34+
if git status --porcelain | grep .; then
35+
echo "changed=true" >> "$GITHUB_OUTPUT"
36+
else
37+
echo "changed=false" >> "$GITHUB_OUTPUT"
38+
fi
39+
40+
- name: Commit and push updates
41+
if: steps.changes.outputs.changed == 'true'
42+
run: |
43+
git config user.name "github-actions[bot]"
44+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
45+
git add user-guide developer-guide
46+
git commit -m "chore: update sidebar highlights for docs"
47+
git push

developer-guide/09-Storage and the filesystem/02-filesystem.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: Important locations on the filesystem
33
slug: /filesystem-locations
44
hide_table_of_contents: true
5+
sidebar_class_name: sidebar-item--updated
56
---
67

78
# Important Raspberry Pi Locations for Pioreactor Images

developer-guide/10-Hardware/10-custom-hardware.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ title: Customize the hardware interface
33
slug: /custom-hardware
44
description: Extend or override Pioreactor hardware definitions by layering YAML files that hardware.py reads.
55
hide_table_of_contents: true
6+
sidebar_class_name: sidebar-item--updated
67
---
78

89
Pioreactor's hardware layer is intentionally data-driven. Everything in [`core/pioreactor/hardware.py`](https://github.com/pioreactor/pioreactor/blob/main/core/pioreactor/hardware.py) loads user-editable YAML files from `~/.pioreactor/hardware/` (or the folder pointed to by the `DOT_PIOREACTOR` env var). By editing these files you can rewire pins, add new peripherals, or describe an entirely new bioreactor model without touching the Python code. Pair these configs with [custom bioreactor model definitions](/developer-guide/custom-bioreactor-models) so the UI, safety limits, and wiring stay in sync.

developer-guide/10-Hardware/11-custom_models.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: Custom bioreactor models
33
slug: /custom-bioreactor-models
44
hide_table_of_contents: true
5+
sidebar_class_name: sidebar-item--updated
56
---
67

78
:::tip

user-guide/03-Extending your Pioreactor/01-cluster-management/03-backup-and-restore-system-files.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ title: Back up and restore Pioreactor system files
44
slug: /backup-system-files
55
description: Export and import a Pioreactor’s ~/.pioreactor directory from the Inventory page.
66
hide_table_of_contents: true
7+
sidebar_class_name: sidebar-item--updated
78
---
89

910
Each Pioreactor keeps its configuration, calibration data, and persistent state inside `~/.pioreactor`. The Inventory page now lets you export that directory as a system archive (`.zip`) for safekeeping and import it onto the same unit when you need to restore or clone a setup.

0 commit comments

Comments
 (0)