Skip to content

Commit 2360883

Browse files
committed
refactor(tools): modernise update-readmes
Bring update-readmes onto the same shape as the other tools and drop a few oddities that were copy-pasted from a plugin skeleton and never cleaned up: - Import _common for iter_check_plugins() and die(). The plugin discovery loop drops from an 8-line `lib.disk.walk_directory` + substring-filter chain down to a list comprehension over `_common.iter_check_plugins(skip=frozenset())`. `skip` is empty on purpose so the example plugin's README still gets its Help block refreshed (keeps prior behaviour). - Add CONTRIBUTING.md header comment and bump __version__ onto the repo's YYYYMMDDXX convention. - Rewrite the module docstring from a plugin-style DESCRIPTION blob into a proper "what does this tool do" explanation. - Drop the `sys.exit(3)` fallback around parse_args(). That exit code only makes sense in a Nagios check plugin where 3 maps to STATE_UNKNOWN; update-readmes is a repo tool and should follow argparse's own exit conventions (0 for -h/-V, 2 for parse errors). - Replace `lib.base.cu()` in the top-level except handler with `_common.die()` for the same reason. cu() emits a Nagios UNKNOWN line which is meaningless for a tool. - Pull the magic numbers out into HELP_TIMEOUT_SECONDS and MAX_PARALLEL_HELP_CALLS so they are easy to tune. - Fix the get_help() return shape: it used to return either a 2-tuple on success or a 3-tuple on error, which forced every caller to `len()`-check the result. It now always returns `(name, help_text_or_None)` and logs the error inline. Verified: `tools/update-readmes` walks 238 READMEs cleanly, reports 0 updates (tree was already up to date), exits 0. tools/run-linter-checks stays green.
1 parent 8b5cd40 commit 2360883

File tree

1 file changed

+94
-65
lines changed

1 file changed

+94
-65
lines changed

tools/update-readmes

Lines changed: 94 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,47 @@
88

99
# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md
1010

11+
"""Refresh the `## Help` section of every check-plugin README.
12+
13+
Each plugin's README carries a fenced code block between the
14+
`## Help` and `## Usage Examples` headings that contains the
15+
plugin's `--help` output verbatim. This tool runs every plugin
16+
with `--help`, captures the output and replaces that one code
17+
block; the rest of the README stays untouched.
18+
19+
Run this after changing a plugin's `argparse` definition (or in
20+
bulk before a release) so the rendered docs at
21+
<https://linuxfabrik.github.io/monitoring-plugins/> stay in sync
22+
with the actual CLI.
23+
"""
24+
1125
import argparse
1226
import concurrent.futures
1327
import re
1428
import subprocess
15-
import sys
29+
30+
import _common
1631

1732
import lib.base
1833
import lib.disk
1934

2035
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
21-
__version__ = '2026041001'
36+
__version__ = '2026041301'
37+
38+
DESCRIPTION = """Refresh the `## Help` section of every check-plugin README by running each plugin
39+
with --help and replacing the fenced code block between `## Help` and `## Usage Examples`."""
2240

23-
DESCRIPTION = """Updates the Help section in all monitoring plugin README files by running
24-
each plugin with --help and replacing the corresponding code block. Run this tool from the
25-
repository root directory."""
41+
HELP_TIMEOUT_SECONDS = 10
42+
MAX_PARALLEL_HELP_CALLS = 16
43+
44+
# The substring search anchors on both the header above and the
45+
# header below the block, so we only replace the plugin's actual
46+
# --help output and never accidentally pick up a later `## Help`
47+
# heading in a different section.
48+
HELP_BLOCK_REGEX = re.compile(
49+
r'## Help\s*```text\s*?(.*?)\s*```\s*## Usage Examples',
50+
re.DOTALL,
51+
)
2652

2753

2854
def parse_args():
@@ -39,84 +65,87 @@ def parse_args():
3965
return parser.parse_args()
4066

4167

42-
def get_help(plugin):
43-
"""Run plugin --help and return (plugin_name, help_text) or (plugin_name, None) on error."""
44-
cmd = f'check-plugins/{plugin}/{plugin}'
68+
def get_help(plugin_dir):
69+
"""Run the plugin with --help and return `(name, help_text_or_None)`.
70+
71+
Errors (missing plugin script, non-zero exit, timeout) are
72+
printed to stderr and the plugin is reported back with a
73+
`None` help text so the caller can skip it.
74+
"""
75+
plugin_name = plugin_dir.name
76+
plugin_script = plugin_dir / plugin_name
4577
try:
4678
result = subprocess.run(
47-
[cmd, '--help'],
79+
[str(plugin_script), '--help'],
4880
capture_output=True,
4981
text=True,
50-
timeout=10,
82+
timeout=HELP_TIMEOUT_SECONDS,
83+
check=False,
5184
)
52-
return (plugin, result.stdout.strip())
53-
except Exception as e:
54-
return (plugin, None, str(e))
85+
except Exception as exc: # pylint: disable=broad-except
86+
_common.err(f'{plugin_name}: failed to run --help ({exc})')
87+
return plugin_name, None
88+
return plugin_name, result.stdout.strip()
5589

5690

57-
def main():
58-
"""The main function. This is where the magic happens."""
91+
def update_readme(plugin_dir, help_text):
92+
"""Replace the Help block in the plugin's README.
5993
60-
try:
61-
parse_args()
62-
except SystemExit:
63-
sys.exit(3)
64-
65-
readmes = lib.disk.walk_directory(
66-
'./',
67-
exclude_pattern=r'',
68-
include_pattern=r'.*README.md',
69-
relative=True,
70-
)
94+
Returns `True` if the file was written, `False` if the file
95+
already matched or if it does not carry the expected
96+
`## Help` / `## Usage Examples` anchors.
97+
"""
98+
readme_path = plugin_dir / 'README.md'
99+
current = lib.base.coe(lib.disk.read_file(str(readme_path)))
100+
replacement = f'## Help\n\n```text\n{help_text}\n```\n\n\n## Usage Examples'
101+
updated = HELP_BLOCK_REGEX.sub(replacement, current)
102+
if updated == current:
103+
return False
104+
lib.base.coe(lib.disk.write_file(str(readme_path), updated))
105+
return True
71106

72-
# collect plugin names from README paths
73-
plugins = []
74-
for readme_path in sorted(readmes):
75-
if 'check-plugins/' not in readme_path:
76-
continue
77-
if readme_path.startswith('docs/'):
78-
continue
79-
plugin = readme_path.replace('check-plugins/', '').replace('/README.md', '')
80-
plugins.append(plugin)
81107

82-
# run all --help calls in parallel
108+
def main():
109+
"""Iterate check-plugins, refresh each README's Help block."""
110+
parse_args()
111+
112+
plugin_dirs = [
113+
p for p in _common.iter_check_plugins(skip=frozenset())
114+
if (p / 'README.md').is_file() and (p / p.name).is_file()
115+
]
116+
83117
help_texts = {}
84-
with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
85-
futures = {executor.submit(get_help, p): p for p in plugins}
118+
with concurrent.futures.ThreadPoolExecutor(
119+
max_workers=MAX_PARALLEL_HELP_CALLS,
120+
) as executor:
121+
futures = {executor.submit(get_help, p): p for p in plugin_dirs}
86122
for future in concurrent.futures.as_completed(futures):
87-
result = future.result()
88-
plugin = result[0]
89-
if len(result) == 3:
90-
print(f'Error "{result[2]}" while calling command "check-plugins/{plugin}/{plugin} --help"')
91-
continue
92-
help_texts[plugin] = result[1]
93-
94-
# update READMEs
123+
plugin_name, help_text = future.result()
124+
if help_text is not None:
125+
help_texts[plugin_name] = help_text
126+
127+
# build the message
95128
updated = 0
96-
for plugin in sorted(plugins):
97-
if plugin not in help_texts:
129+
skipped = 0
130+
for plugin_dir in plugin_dirs:
131+
help_text = help_texts.get(plugin_dir.name)
132+
if help_text is None:
133+
skipped += 1
98134
continue
99-
_help = help_texts[plugin]
100-
101-
readme_path = f'check-plugins/{plugin}/README.md'
102-
readme = lib.base.coe(lib.disk.read_file(readme_path))
103-
new_readme = re.sub(
104-
r'## Help\s*```text\s*?(.*?)\s*```\s*## Usage Examples',
105-
f'## Help\n\n```text\n{_help}\n```\n\n\n## Usage Examples',
106-
readme,
107-
flags=re.DOTALL,
108-
)
109-
110-
if new_readme != readme:
111-
lib.base.coe(lib.disk.write_file(readme_path, new_readme))
112-
print(f'Updated {readme_path}')
135+
if update_readme(plugin_dir, help_text):
136+
print(f'Updated check-plugins/{plugin_dir.name}/README.md')
113137
updated += 1
114138

115-
print(f'\n{updated} READMEs updated, {len(plugins) - updated} unchanged.')
139+
unchanged = len(plugin_dirs) - updated - skipped
140+
print(
141+
f'\n{updated} READMEs updated, '
142+
f'{unchanged} unchanged, '
143+
f'{skipped} skipped (help errors).',
144+
)
116145

117146

118147
if __name__ == '__main__':
119148
try:
120149
main()
121-
except Exception:
122-
lib.base.cu()
150+
except Exception: # pylint: disable=broad-except
151+
_common.die('update-readmes: unexpected error, aborting')

0 commit comments

Comments
 (0)