From 5c612ec345392cc7b7020cc85e12fc9e128f9f26 Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Tue, 22 Sep 2026 16:40:47 +0300 Subject: [PATCH 1/5] Added broken internal link checker Run it on the postprocess stage when we have bilt HTML files. Ticket: ENT-14135 Signed-off-by: Ihor Aleksandrychiev (cherry picked from commit aa4f0f7b3f2b4ffa550efff76bb980185bdc0b92) --- generator/_scripts/cfdoc_link_checker.py | 126 +++++++++++++++++++++++ generator/_scripts/cfdoc_postprocess.py | 6 ++ 2 files changed, 132 insertions(+) create mode 100644 generator/_scripts/cfdoc_link_checker.py diff --git a/generator/_scripts/cfdoc_link_checker.py b/generator/_scripts/cfdoc_link_checker.py new file mode 100644 index 0000000000..60b622fdfe --- /dev/null +++ b/generator/_scripts/cfdoc_link_checker.py @@ -0,0 +1,126 @@ +import html.parser +import os +import re +import sys +import urllib.parse + +# Tags/attributes that carry a URL we should be able to resolve to a file. +LINK_ATTRS = { + "a": "href", + "img": "src", + "link": "href", + "script": "src", +} + +# Schemes that never point at a file in the built site. +SKIPPED_SCHEMES = ("mailto:", "tel:", "javascript:", "data:") + +# The version switcher (lts_versions_list.html / versions_list.html) links to +# sibling docs builds for other branches/versions, e.g. "../../docs/3.27/", +# "../../docs/lts/" or "../../docs/archive/index.html". Each version/archive +# is built and deployed separately, so those paths never exist in this +# build's own _site and aren't broken links. +VERSION_LINK_RE = re.compile( + r"(?:^|/)docs/(?:master|lts|archive|\d+(?:\.\d+){1,2})(?:/|$)" +) + + +class _LinkExtractor(html.parser.HTMLParser): + """Collects (url, line) for every href/src found in one HTML page.""" + + def __init__(self): + super().__init__(convert_charrefs=True) + self.links = [] + + def handle_starttag(self, tag, attrs): + attr_name = LINK_ATTRS.get(tag) + if attr_name is None: + return + url = dict(attrs).get(attr_name) + if url: + line, _col = self.getpos() + self.links.append((url, line)) + + +def _is_checkable(url): + """Only internal links/assets are worth resolving against the filesystem.""" + url = url.strip() + if not url or url.startswith("#"): + return False + if url.startswith(SKIPPED_SCHEMES): + return False + if VERSION_LINK_RE.search(url): + return False + parsed = urllib.parse.urlsplit(url) + if parsed.scheme or parsed.netloc: + return False # external, e.g. http://, https://, //host/... + return True + + +def _resolve(site_root, html_file, url): + """Map an internal href/src to the file it should point at on disk. + + Returns None for links that don't reference a separate file (e.g. a bare + "?query" link on the current page). + """ + path = url.split("#", 1)[0].split("?", 1)[0] + if not path: + return None + + if path.startswith("/"): + target = os.path.join(site_root, path.lstrip("/")) + else: + target = os.path.normpath(os.path.join(os.path.dirname(html_file), path)) + + # Hugo renders pretty URLs as a directory containing index.html; a link + # is valid whether or not it has the trailing slash the directory implies. + if os.path.isdir(target): + target = os.path.join(target, "index.html") + + return target + + +def _find_html_files(site_root): + for dirpath, _dirnames, filenames in os.walk(site_root): + for filename in filenames: + if filename.endswith(".html"): + yield os.path.join(dirpath, filename) + + +def run(config): + """Checks every internal link/image/script src in the built site and + exits non-zero, listing what's broken and where, if any target is + missing. + + Runs against the generated HTML in CFE_DIR (after the Hugo build) rather + than the markdown source, so it validates Hugo's actual routing (pretty + URLs, aliases, page bundles) instead of a reimplementation of it. + """ + site_root = os.path.join(config["project_directory"], config["CFE_DIR"]) + if not os.path.isdir(site_root): + sys.stderr.write("ERROR: built site not found at %s\n" % site_root) + sys.exit(1) + + broken = [] + for html_file in _find_html_files(site_root): + parser = _LinkExtractor() + with open(html_file, "r", encoding="utf-8", errors="replace") as f: + parser.feed(f.read()) + + for url, line in parser.links: + if not _is_checkable(url): + continue + target = _resolve(site_root, html_file, url) + if target is None: + continue + if not os.path.exists(target): + broken.append((os.path.relpath(html_file, site_root), line, url)) + + if broken: + sys.stderr.write( + "ERROR: %d broken internal link(s)/asset reference(s) found in the built site:\n" + % len(broken) + ) + for page, line, url in broken: + sys.stderr.write(" %s:%d: %s\n" % (page, line, url)) + sys.exit(1) diff --git a/generator/_scripts/cfdoc_postprocess.py b/generator/_scripts/cfdoc_postprocess.py index a2bfe995b6..fe054528a4 100755 --- a/generator/_scripts/cfdoc_postprocess.py +++ b/generator/_scripts/cfdoc_postprocess.py @@ -24,6 +24,7 @@ import cfdoc_environment as environment import cfdoc_sourcelinks as sourcelinks +import cfdoc_link_checker as link_checker import sys @@ -35,4 +36,9 @@ print(sys.exc_info()) exit(1) +# Runs outside the try/except above: it exits directly (like +# cfdoc_references_resolver.run) with a clean broken-link report, rather than +# letting the generic exception handler above swallow that message. +link_checker.run(config) + exit(0) From bb989b55bad5d0f7cb6cd1753578cc1a6b61d765 Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Tue, 22 Sep 2026 17:55:33 +0300 Subject: [PATCH 2/5] Fixed broken links Signed-off-by: Ihor Aleksandrychiev (cherry picked from commit 6e6b41b7557b3f1070e8e5150fe14b48d521e1a2) --- content/examples/tutorials/cfbs.markdown | 2 +- content/examples/tutorials/file_comparison.markdown | 2 +- .../tutorials/policy-writing}/hugo-commit.png | Bin .../promise-type-module-development.markdown | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename content/{getting-started => examples/tutorials/policy-writing}/hugo-commit.png (100%) diff --git a/content/examples/tutorials/cfbs.markdown b/content/examples/tutorials/cfbs.markdown index cb6cbca3f2..d0f5655a0c 100644 --- a/content/examples/tutorials/cfbs.markdown +++ b/content/examples/tutorials/cfbs.markdown @@ -4,7 +4,7 @@ title: Using CFEngine Build command line tools - cfbs --- In this tutorial, we'll take a look at how to work with CFEngine Build projects from the command line. -We assume you have already set up a hub, and installed [`cf-remote`](https://pypi.org/project/cf-remote/) and [`cfbs`](https://pypi.org/project/cfbs/) as we do in [step 1 of the getting started guide](/getting-started/01-installation/). +We assume you have already set up a hub, and installed [`cf-remote`](https://pypi.org/project/cf-remote/) and [`cfbs`](https://pypi.org/project/cfbs/) as we do in [step 1 of the getting started guide](/getting-started/01-installing-cfengine/). When working on a CFEngine Build project, the workflow looks like this: diff --git a/content/examples/tutorials/file_comparison.markdown b/content/examples/tutorials/file_comparison.markdown index 56aec2fab2..9535a4d2b5 100644 --- a/content/examples/tutorials/file_comparison.markdown +++ b/content/examples/tutorials/file_comparison.markdown @@ -6,7 +6,7 @@ aliases: - "/examples-tutorials-file_comparison.html" --- -1. Add the [policy contents][File comparison#Full policy] (also can be downloaded from file_compare_test.cf) to a new file, such as /var/cfengine/masterfiles/file_test.cf. +1. Add the [policy contents][File comparison#Full policy] (also can be downloaded from file_compare_test.cf) to a new file, such as /var/cfengine/masterfiles/file_test.cf. 2. Run the following commands as root on the command line: ```console diff --git a/content/getting-started/hugo-commit.png b/content/examples/tutorials/policy-writing/hugo-commit.png similarity index 100% rename from content/getting-started/hugo-commit.png rename to content/examples/tutorials/policy-writing/hugo-commit.png diff --git a/content/examples/tutorials/promise-type-module-development.markdown b/content/examples/tutorials/promise-type-module-development.markdown index 27e1991d53..5ebc79b4b6 100644 --- a/content/examples/tutorials/promise-type-module-development.markdown +++ b/content/examples/tutorials/promise-type-module-development.markdown @@ -137,6 +137,6 @@ There are several places to look for more information or inspiration when writin - [The real git promise type code](https://github.com/cfengine/modules/tree/c3b7329b240cf7ad062a0a64ee8b607af2cb912a/promise-types/git/) - [HTTP promise type module](https://github.com/cfengine/modules/tree/c861789d4b376147d904fccd76963a92e65eaa97/promise-types/http/) -- [CFEngine custom promise type specification](./reference-promise-types-custom.html) +- [CFEngine custom promise type specification](/reference/promise-types/custom/) - [Blog post: How to implement CFEngine Custom Promise types in Python](https://cfengine.com/blog/2020/how-to-implement-cfengine-custom-promise-types-in-python/) - [Blog post: How to implement CFEngine Custom Promise types in Bash](https://cfengine.com/blog/2021/how-to-implement-cfengine-custom-promise-types-in-bash/) From f2ea38605d325a87dbde1e901c2f3bfced44e0bd Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Wed, 23 Sep 2026 16:11:16 +0300 Subject: [PATCH 3/5] Fixed linting issues Signed-off-by: Ihor Aleksandrychiev --- content/reference/promise-types/files/_index.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/reference/promise-types/files/_index.markdown b/content/reference/promise-types/files/_index.markdown index 45db130c9c..5ad1d9b2d4 100644 --- a/content/reference/promise-types/files/_index.markdown +++ b/content/reference/promise-types/files/_index.markdown @@ -234,7 +234,7 @@ files: body classes if_ok(x) { promise_repaired => { "$(x)" }; - promise_kept => { "$(x)" }; +promise_kept => { "$(x)" }; } @@ -260,7 +260,7 @@ file_result => "leaf_name"; body classes if_ok(x) { promise_repaired => { "$(x)" }; - promise_kept => { "$(x)" }; +promise_kept => { "$(x)" }; } From 51ad1a1addde8f390ca1bfa76bf293d1d2274098 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Tue, 1 Sep 2026 07:37:08 -0500 Subject: [PATCH 4/5] Fixed reference labels that resolved to 404s (cherry picked from commit 2140a44fea802e705b9e8a9ecf0e0364f5e1ff86) --- ...own => cfe_internal-cfe_cfengine.markdown} | 0 generator/_references.md | 93 +++++++++---------- 2 files changed, 42 insertions(+), 51 deletions(-) rename content/reference/masterfiles-policy-framework/{cfe_internal-CFE_cfengine.markdown => cfe_internal-cfe_cfengine.markdown} (100%) diff --git a/content/reference/masterfiles-policy-framework/cfe_internal-CFE_cfengine.markdown b/content/reference/masterfiles-policy-framework/cfe_internal-cfe_cfengine.markdown similarity index 100% rename from content/reference/masterfiles-policy-framework/cfe_internal-CFE_cfengine.markdown rename to content/reference/masterfiles-policy-framework/cfe_internal-cfe_cfengine.markdown diff --git a/generator/_references.md b/generator/_references.md index e8d5928cbe..925b039803 100644 --- a/generator/_references.md +++ b/generator/_references.md @@ -11,66 +11,57 @@ [github documentation]: https://github.com/cfengine/documentation "Documentation repository on GitHub" [github design-center]: https://github.com/cfengine/design-center "Design-Center repository on GitHub" [evaluate cfengine]: https://cfengine.com/evaluate-enterprise "Evaluate CFEngine" -[cfengine getting started]: https://cfengine.com/enterprise-getting-started "Get started in 10 minutes" [LMDB]: http://symas.com/mdb/ "Symas Lightning Memory-Mapped Database" -[vim_cf3]: https://github.com/neilhwatson/vim_cf3 "CFEngine 3 vim integration" [community package repositories]: http://cfengine.com/cfengine-linux-distros "Community package repositories" [community download page]: https://cfengine.com/community/download/ "Community package download page" [enterprise software download page]: https://cfengine.com/product/free-download/ "Enterprise download page" -[boolean]: reference-language-concepts-promises.html#promise-attributes -[clist]: reference-language-concepts-promises.html#promise-attributes -[anchored]: reference-language-concepts-pattern-matching-and-referencing.html#anchored-vs-unanchored-regular-expressions -[unanchored]: reference-language-concepts-pattern-matching-and-referencing.html#anchored-vs-unanchored-regular-expressions -[datatypes]: reference-language-concepts-variables.html -[utility functions]: tags-utility-functions.html -[data functions]: tags-data-functions.html -[system functions]: tags-system-functions.html -[io functions]: tags-io-functions.html -[comm functions]: tags-communication-functions.html -[files functions]: tags-files-functions.html -[body classes]: reference-promise-types.html#classes -[body common]: reference-components.html#common-control -[body file]: reference-language-concepts-namespaces.html -[body hub control port]: reference-components-cf-hub.html#port -[sys.policy_entry_dirname]: reference-special-variables-sys.html#sys-policy_entry_dirname -[sys.policy_entry_filename]: reference-special-variables-sys.html#sys-policy_entry_filename +[boolean]: /reference/language-concepts/promises#promise-attributes +[clist]: /reference/language-concepts/promises#promise-attributes +[anchored]: /reference/language-concepts/pattern-matching-and-referencing#anchored-vs-unanchored-regular-expressions +[unanchored]: /reference/language-concepts/pattern-matching-and-referencing#anchored-vs-unanchored-regular-expressions +[datatypes]: /reference/language-concepts/variables +[body classes]: /reference/promise-types#classes +[body common]: /reference/components#common-control +[body file]: /reference/language-concepts/namespaces +[body hub control port]: /reference/components/cf-hub#port +[sys.policy_entry_dirname]: /reference/special-variables/sys#syspolicy_entry_dirname +[sys.policy_entry_filename]: /reference/special-variables/sys#syspolicy_entry_filename [supported versions]: https://cfengine.com/extended-support/ -[Inventory API]: api-enterprise-api-ref-inventory.html -[sys.uqhost]: reference-special-variables-sys.html#sys-uqhost -[sys.policy_hub]: reference-special-variables-sys.html#sys-policy_hub -[seed_cp]: reference-masterfiles-policy-framework-lib-files.html#seed_cp -[Masterfiles Policy Framework]: reference-masterfiles-policy-framework.html -[Append to inputs used by main policy]: reference-masterfiles-policy-framework.html#append-to-inputs-used-by-main-policy -[mpf_extra_autorun_inputs]: reference-masterfiles-policy-framework.html#additional-automatically-loaded-inputs -[Append to inputs used by update policy]: reference-masterfiles-policy-framework.html#append-to-inputs-used-by-update-policy -[Classes and decisions]: reference-language-concepts-classes.html -[language-concepts-classes-hard]: reference-language-concepts-classes.html#hard-classes.html "Language concepts -> Classes and decisions: Hard classes" -[lib/files.cf]: reference-masterfiles-policy-framework-lib-files -[lib/packages.cf]: reference-masterfiles-policy-framework-lib-packages -[stdlib-mog]: reference-masterfiles-policy-framework-lib-files.html#mog +[Inventory API]: /api/enterprise-api-ref/inventory +[sys.uqhost]: /reference/special-variables/sys#sysuqhost +[sys.policy_hub]: /reference/special-variables/sys#syspolicy_hub +[seed_cp]: /reference/masterfiles-policy-framework/lib-files#seed_cp +[Masterfiles Policy Framework]: /reference/masterfiles-policy-framework +[Append to inputs used by main policy]: /reference/masterfiles-policy-framework#append-to-inputs-used-by-main-policy +[Append to inputs used by update policy]: /reference/masterfiles-policy-framework#add-additional-policy-files-for-update-inputs +[Classes and decisions]: /reference/language-concepts/classes +[language-concepts-classes-hard]: /reference/language-concepts/classes#hard-classes "Language concepts -> Classes and decisions: Hard classes" +[lib/files.cf]: /reference/masterfiles-policy-framework/lib-files +[lib/packages.cf]: /reference/masterfiles-policy-framework/lib-packages +[stdlib-mog]: /reference/masterfiles-policy-framework/lib-files#mog [jq-project]: https://stedolan.github.io/jq/ "jq is a lightweight and flexible command-line JSON processor. Try online at jqplay.org!" -[Using Vagrant]: getting-started-installation-general-installation-installation-enterprise-vagrant.html "The CFEngine Vagrant environment provides an easy way to test and explore CFEngine Enterprise." -[type]: reference-functions-type.html -[promise-type-measurements]: reference-promise-types-measurements.html -[promise-type-custom]: /reference/promise-types/custom.html -[promise-type-custom-protocol]: reference-promise-types-custom.html#protocol -[component-cf-monitord]: reference-components-cf-monitord.html -[cf-hub]: reference-components-cf-hub.html -[cf-hub#hub_schedule]: reference-components-cf-hub.html#hub_schedule -[cf-hub#exclude_hosts]: reference-components-cf-hub.html#exclude_hosts -[cf-hub#control-promises]: reference-components-cf-hub.html#control-promises -[mpf-configure-measurement-collection]: reference-masterfiles-policy-framework.html#configure-enterprise-measurement-monitoring-collection.html -[mpf-configure-component-restart]: reference-masterfiles-policy-framework.html#configure-mpf-to-automatically-restart-components-on-relevant-data-change.html -[mpf-classification-bundles]: reference-masterfiles-policy-framework.html#classification-bundles-before-autorun.html -[mpf-services-autorun]: reference-masterfiles-policy-framework-services-autorun.html -[package-modules-the-api]: reference-language-concepts-modules-package-module-api.html#the-api -[Functions#collecting functions]: /reference/functions/#collecting-functions -[guest_environments]: /reference/promise-types/guest_environments.html -[defaults]: /reference/promise-types/defaults.html +[Using Vagrant]: /getting-started/01-installing-cfengine/general-installation/installation-enterprise-vagrant "The CFEngine Vagrant environment provides an easy way to test and explore CFEngine Enterprise." +[type]: /reference/functions/type +[promise-type-measurements]: /reference/promise-types/measurements +[promise-type-custom]: /reference/promise-types/custom +[promise-type-custom-protocol]: /reference/promise-types/custom#protocol +[component-cf-monitord]: /reference/components/cf-monitord +[cf-hub]: /reference/components/cf-hub +[cf-hub#hub_schedule]: /reference/components/cf-hub#hub_schedule +[cf-hub#exclude_hosts]: /reference/components/cf-hub#exclude_hosts +[cf-hub#control-promises]: /reference/components/cf-hub#control-promises +[mpf-configure-measurement-collection]: /reference/masterfiles-policy-framework#configure-enterprise-measurementmonitoring-collection +[mpf-configure-component-restart]: /reference/masterfiles-policy-framework#configure-mpf-to-automatically-restart-components-on-relevant-data-change +[mpf-classification-bundles]: /reference/masterfiles-policy-framework#classification-bundles-before-autorun +[mpf-services-autorun]: /reference/masterfiles-policy-framework/services-autorun +[package-modules-the-api]: /reference/language-concepts/modules/package-module-api#the-api +[Functions#collecting functions]: /reference/functions#collecting-functions +[guest_environments]: /reference/promise-types/guest_environments +[defaults]: /reference/promise-types/defaults [getgrgid()]: https://linux.die.net/man/3/getgrgid [getgrnam()]: https://linux.die.net/man/3/getgrnam [getpwuid()]: https://linux.die.net/man/3/getpwuid [getpwnam()]: https://linux.die.net/man/3/getpwnam [select()]: https://linux.die.net/man/3/select [search_up]: /reference/functions/findfiles_up -[High availability]: /examples/tutorials/high-availability/ "CFEngine High availability overview" +[High availability]: /examples/tutorials/high-availability "CFEngine High availability overview" From 7067274ee8019c4e7ddec29273d539a825361192 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Tue, 1 Sep 2026 07:37:11 -0500 Subject: [PATCH 5/5] Fixed inline links written with a legacy .html target (cherry picked from commit fbd4bd360c65e36822d8fb8c18771b3f2a021f5e) --- .../promise-types/files/edit_line/insert_lines.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/reference/promise-types/files/edit_line/insert_lines.markdown b/content/reference/promise-types/files/edit_line/insert_lines.markdown index 4477595279..5046fb929d 100644 --- a/content/reference/promise-types/files/edit_line/insert_lines.markdown +++ b/content/reference/promise-types/files/edit_line/insert_lines.markdown @@ -350,7 +350,7 @@ body insert_select example **Type:** `body location` -**See also:** [Common body attributes][Promise types#Common body attributes], [`location` bodies in the standard library](reference-masterfiles-policy-framework-lib-files.html#location-bodies), [`start` location body in the standard library](reference-masterfiles-policy-framework-lib-files.html#location-bodies), [`before(srt)` location body in the standard library](reference-masterfiles-policy-framework-lib-files.html#before), [`after(srt)` location body in the standard library](reference-masterfiles-policy-framework-lib-files.html#after) +**See also:** [Common body attributes][Promise types#Common body attributes], [`location` bodies in the standard library](/reference/masterfiles-policy-framework/lib-files#location-bodies), [`start` location body in the standard library](/reference/masterfiles-policy-framework/lib-files#location-bodies), [`before(srt)` location body in the standard library](/reference/masterfiles-policy-framework/lib-files#before), [`after(srt)` location body in the standard library](/reference/masterfiles-policy-framework/lib-files#after) #### before_after