From ab4b8351261d62e4319ad743473a0bb96d9b7dd9 Mon Sep 17 00:00:00 2001 From: atif09 Date: Sun, 18 Jan 2026 01:07:23 +0530 Subject: [PATCH 01/10] [build] Enable building docs with local changes #251 Contributors could not preview local documentation changes because the build system always cloned from GitHub instead of using local files. This fix modifies build.py to detect local openwisp-docs builds and symlink local files to staging-dir/ instead of cloning. This works only in non-PRODUCTION mode, so CI/CD behavior is unchanged. Also fixed git fetch/checkout commands to work with branches and tags. Fixes #251 --- build.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/build.py b/build.py index e0e8b226..2142f7b5 100755 --- a/build.py +++ b/build.py @@ -271,6 +271,32 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): If the repository already exists, update it. Otherwise, clone the repository. """ repository = f"{owner}/{name}" + + # Support for building with local changes + if name == "openwisp-docs" and not os.environ.get("PRODUCTION"): + print(f"Using local directory for '{name}'") + + os.makedirs("staging-dir", exist_ok=True) + exclude_items = {"staging-dir", "modules", "_build", ".git", "__pycache__"} + for item in os.listdir("."): + if item.startswith("."): + continue + + if item in exclude_items: + continue + + # skip virtual environments (detect with pyvenv.cfg) + if os.path.isdir(item) and os.path.exists(os.path.join(item, "pyvenv.cfg")): + continue + + src_path = os.path.abspath(item) + dest_path = os.path.join("staging-dir", item) + if os.path.islink(dest_path): + os.unlink(dest_path) + if not os.path.exists(dest_path): + os.symlink(src_path, dest_path) + return + if os.environ.get("SSH"): # SSH cloning is a convenient option for local development, as it # allows you to commit changes directly to the repository, but it @@ -294,7 +320,13 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): # "-c advice.detachedHead=false" is used to suppress the warning # about being in a detached HEAD state when checking out tags. subprocess.run( - ["git", "-c", "advice.detachedHead=false", "checkout", f"refs/heads/{branch}"], + [ + "git", + "-c", + "advice.detachedHead=false", + "checkout", + branch, + ], cwd=clone_path, check=True, ) From d98c821887fa1cdc8062a9dc4e3e25d55bd36248 Mon Sep 17 00:00:00 2001 From: atif09 Date: Sun, 18 Jan 2026 02:22:51 +0530 Subject: [PATCH 02/10] [build] Add safeguard for staging-dir symlink edge case As suggested by CodeRabbit review, add check to ensure staging-dir is a real directory before creating symlinks inside it. --- build.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index 2142f7b5..2a228849 100755 --- a/build.py +++ b/build.py @@ -271,11 +271,12 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): If the repository already exists, update it. Otherwise, clone the repository. """ repository = f"{owner}/{name}" - # Support for building with local changes if name == "openwisp-docs" and not os.environ.get("PRODUCTION"): print(f"Using local directory for '{name}'") + if os.path.islink("staging-dir") or os.path.isfile("staging-dir"): + os.unlink("staging-dir") os.makedirs("staging-dir", exist_ok=True) exclude_items = {"staging-dir", "modules", "_build", ".git", "__pycache__"} for item in os.listdir("."): @@ -325,7 +326,7 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): "-c", "advice.detachedHead=false", "checkout", - branch, + f"refs/heads/{branch}", ], cwd=clone_path, check=True, From 4b9525a60ae33658cbea24aab37b8661bb9e638b Mon Sep 17 00:00:00 2001 From: atif09 Date: Mon, 19 Jan 2026 00:56:50 +0530 Subject: [PATCH 03/10] [build] Fix git fetch for repeated builds #251 The original refspec tried to fetch directly into checked-out branches, causing 'refusing to fetch into checked-out branch' errors on repeated builds This fix fetches into remote-tracking branches (refs/remotes/origin/) instead, which can always be updated, then uses 'checkout -B' to create/reset the local branch from the remote. This maintains branch/tag disambiguation while fixing the repeated build issue Fixes #251 --- build.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/build.py b/build.py index 2a228849..8c113ea7 100755 --- a/build.py +++ b/build.py @@ -313,8 +313,13 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): if os.path.exists(clone_path): print(f"Repository '{name}' already exists. Updating...") subprocess.run( - # Clears ambiguity when git tags and branches have identical names - ["git", "fetch", "origin", f"refs/heads/{branch}:refs/heads/{branch}"], + # Update remote-tracking ref (avoid fetching into checked-out branch) + [ + "git", + "fetch", + "origin", + f"+refs/heads/{branch}:refs/remotes/origin/{branch}", + ], cwd=clone_path, check=True, ) @@ -326,7 +331,9 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): "-c", "advice.detachedHead=false", "checkout", - f"refs/heads/{branch}", + "-B", + branch, + f"refs/remotes/origin/{branch}", ], cwd=clone_path, check=True, From 1749f96d0e970aec1cd10d3a9facd2bb06b3ef8b Mon Sep 17 00:00:00 2001 From: atif09 Date: Thu, 29 Jan 2026 15:03:26 +0530 Subject: [PATCH 04/10] [fix] Addressing feedback #251 Removed redundant print statement and blank lines Fixes #251 --- build.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/build.py b/build.py index 8c113ea7..82770861 100755 --- a/build.py +++ b/build.py @@ -272,9 +272,7 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): """ repository = f"{owner}/{name}" # Support for building with local changes - if name == "openwisp-docs" and not os.environ.get("PRODUCTION"): - print(f"Using local directory for '{name}'") - + if name == "openwisp-docs": if os.path.islink("staging-dir") or os.path.isfile("staging-dir"): os.unlink("staging-dir") os.makedirs("staging-dir", exist_ok=True) @@ -282,14 +280,11 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): for item in os.listdir("."): if item.startswith("."): continue - if item in exclude_items: continue - # skip virtual environments (detect with pyvenv.cfg) if os.path.isdir(item) and os.path.exists(os.path.join(item, "pyvenv.cfg")): continue - src_path = os.path.abspath(item) dest_path = os.path.join("staging-dir", item) if os.path.islink(dest_path): From bc1aa8f81643fed351d7f63b633410ace56da0f7 Mon Sep 17 00:00:00 2001 From: atif09 Date: Thu, 29 Jan 2026 16:01:43 +0530 Subject: [PATCH 05/10] [fix] Addressing missed code rabbit feedback #251 Fully reset staging-dir and honor docs/ convention Fixes #251 --- build.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/build.py b/build.py index 82770861..7613e727 100755 --- a/build.py +++ b/build.py @@ -8,6 +8,7 @@ import argparse import os +import shutil import subprocess from copy import deepcopy @@ -273,19 +274,26 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): repository = f"{owner}/{name}" # Support for building with local changes if name == "openwisp-docs": + # Ensure staging-dir is a real, empty directory if os.path.islink("staging-dir") or os.path.isfile("staging-dir"): os.unlink("staging-dir") + elif os.path.isdir("staging-dir"): + shutil.rmtree("staging-dir") os.makedirs("staging-dir", exist_ok=True) + + base_dir = "docs" if os.path.isdir("docs") else "." exclude_items = {"staging-dir", "modules", "_build", ".git", "__pycache__"} - for item in os.listdir("."): + for item in os.listdir(base_dir): if item.startswith("."): continue if item in exclude_items: continue # skip virtual environments (detect with pyvenv.cfg) - if os.path.isdir(item) and os.path.exists(os.path.join(item, "pyvenv.cfg")): + if os.path.isdir(os.path.join(base_dir, item)) and os.path.exists( + os.path.join(base_dir, item, "pyvenv.cfg") + ): continue - src_path = os.path.abspath(item) + src_path = os.path.abspath(os.path.join(base_dir, item)) dest_path = os.path.join("staging-dir", item) if os.path.islink(dest_path): os.unlink(dest_path) From 662b074cda632449be85def94c0b8b47137ac67e Mon Sep 17 00:00:00 2001 From: atif09 Date: Thu, 29 Jan 2026 16:55:54 +0530 Subject: [PATCH 06/10] [fix] Addressing missed code rabbit feedback #251 - Fix get fetch/checkout to support both branches and tags - Fix git pull to explicitly specify remote and branch Fixes #251 --- build.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.py b/build.py index 7613e727..3df6b21c 100755 --- a/build.py +++ b/build.py @@ -321,7 +321,7 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): "git", "fetch", "origin", - f"+refs/heads/{branch}:refs/remotes/origin/{branch}", + branch, ], cwd=clone_path, check=True, @@ -336,7 +336,7 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): "checkout", "-B", branch, - f"refs/remotes/origin/{branch}", + "FETCH_HEAD", ], cwd=clone_path, check=True, @@ -347,7 +347,7 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): # During local development, we attempt to pull updates, but only if the # current HEAD is on a branch (i.e., not detached, such as when on a tag). if not os.environ.get("PRODUCTION", False) and git_is_on_branch(clone_path): - subprocess.run(["git", "pull"], cwd=clone_path, check=True) + subprocess.run(["git", "pull", "origin", branch], cwd=clone_path, check=True) else: print(f"Cloning repository '{name}'...") subprocess.run( From 09a987979025215fb59a54c56476060eb44e0e6b Mon Sep 17 00:00:00 2001 From: atif09 Date: Thu, 29 Jan 2026 17:58:49 +0530 Subject: [PATCH 07/10] [fix] Add PRODUCTION check and branch filter (master only) #251 The PRODUCTION check and branch filter ensure: - Local dev/master builds: use symlinks for testing local changes - Local old version builds (22.05, etc.): clone from GitHub - CI builds (PRODUCTION=1): clone from GitHub for all versions Fixes #251 --- build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.py b/build.py index 3df6b21c..1bb719e9 100755 --- a/build.py +++ b/build.py @@ -273,7 +273,7 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): """ repository = f"{owner}/{name}" # Support for building with local changes - if name == "openwisp-docs": + if name == "openwisp-docs" and branch == "master" and not os.environ.get("PRODUCTION"): # Ensure staging-dir is a real, empty directory if os.path.islink("staging-dir") or os.path.isfile("staging-dir"): os.unlink("staging-dir") From 8a41477854b7f03e8bed2faf780b4f9ade6908e2 Mon Sep 17 00:00:00 2001 From: atif09 Date: Thu, 29 Jan 2026 16:55:54 +0530 Subject: [PATCH 08/10] [fix] Ran build locally without PRODUCTION check and other minor fixes related to formatting #251 - Removed PRODUCTION check as advised - Improved format Fixes #251 --- build.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index 1bb719e9..1e046dc5 100755 --- a/build.py +++ b/build.py @@ -273,7 +273,7 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): """ repository = f"{owner}/{name}" # Support for building with local changes - if name == "openwisp-docs" and branch == "master" and not os.environ.get("PRODUCTION"): + if name == "openwisp-docs": # Ensure staging-dir is a real, empty directory if os.path.islink("staging-dir") or os.path.isfile("staging-dir"): os.unlink("staging-dir") @@ -347,7 +347,9 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): # During local development, we attempt to pull updates, but only if the # current HEAD is on a branch (i.e., not detached, such as when on a tag). if not os.environ.get("PRODUCTION", False) and git_is_on_branch(clone_path): - subprocess.run(["git", "pull", "origin", branch], cwd=clone_path, check=True) + subprocess.run( + ["git", "pull", "origin", branch], cwd=clone_path, check=True + ) else: print(f"Cloning repository '{name}'...") subprocess.run( From 3bea3c21bbe70dc35406cda8aefcad79ecf3b439 Mon Sep 17 00:00:00 2001 From: atif09 Date: Fri, 30 Jan 2026 08:22:45 +0530 Subject: [PATCH 09/10] [fix] Use local files only for dev version builds #251 Restrict local file symlinks to dev version only, historical versions now clone from their respective branches to ensure correct module structure Fixes #251 --- build.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build.py b/build.py index 1e046dc5..61746022 100755 --- a/build.py +++ b/build.py @@ -266,7 +266,9 @@ def git_is_on_branch(repo_path): return result.stdout.strip() != "HEAD" -def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): +def clone_or_update_repo( + name, branch, dir_name, owner="openwisp", dest=None, version_name=None +): """ Clone or update a repository based on the module name and branch provided. If the repository already exists, update it. Otherwise, clone the repository. @@ -279,8 +281,8 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): os.unlink("staging-dir") elif os.path.isdir("staging-dir"): shutil.rmtree("staging-dir") + if name == "openwisp-docs" and version_name == "dev": os.makedirs("staging-dir", exist_ok=True) - base_dir = "docs" if os.path.isdir("docs") else "." exclude_items = {"staging-dir", "modules", "_build", ".git", "__pycache__"} for item in os.listdir(base_dir): @@ -300,7 +302,6 @@ def clone_or_update_repo(name, branch, dir_name, owner="openwisp", dest=None): if not os.path.exists(dest_path): os.symlink(src_path, dest_path) return - if os.environ.get("SSH"): # SSH cloning is a convenient option for local development, as it # allows you to commit changes directly to the repository, but it @@ -439,6 +440,7 @@ def main(): name="openwisp-docs", branch=docs_branch, dir_name="openwisp-docs", + version_name=version_name, ) for module in modules: clone_or_update_repo( From 885390db7c993dcd1963d90ae9c68b3199a2e1d1 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Fri, 30 Jan 2026 20:13:46 -0300 Subject: [PATCH 10/10] [chores] Minor improvemnts --- build.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/build.py b/build.py index 61746022..d4458edc 100755 --- a/build.py +++ b/build.py @@ -281,14 +281,29 @@ def clone_or_update_repo( os.unlink("staging-dir") elif os.path.isdir("staging-dir"): shutil.rmtree("staging-dir") + # If dev version, copy all doc files to the staging dir if name == "openwisp-docs" and version_name == "dev": os.makedirs("staging-dir", exist_ok=True) base_dir = "docs" if os.path.isdir("docs") else "." - exclude_items = {"staging-dir", "modules", "_build", ".git", "__pycache__"} + exclude_items = ("staging-dir", "modules", "version_switcher") for item in os.listdir(base_dir): - if item.startswith("."): - continue - if item in exclude_items: + is_dir = os.path.isdir(os.path.join(base_dir, item)) + if any( + ( + # not a dir nor a rst file, nor the spelling wordlist + all( + ( + not is_dir, + not item.endswith(".rst"), + item != "spelling_wordlist.txt", + ) + ), + # it's a dir but not designed to contain rst files + is_dir and item.startswith((".", "_")), + # in the exclude list + item in exclude_items, + ) + ): continue # skip virtual environments (detect with pyvenv.cfg) if os.path.isdir(os.path.join(base_dir, item)) and os.path.exists(