Skip to content

Commit dc6dbd9

Browse files
committed
Detect and offer to update outdated build.sh/view.sh from bblocks-template
Bake a shallow clone of bblocks-template into the Docker image at build time so postprocessing can compare a repo's build.sh/view.sh against the current template without a runtime network fetch. A file is only treated as a candidate for refresh if its content has never changed since it was added to the repo's git history (ignoring mode-only commits like chmod); otherwise it's assumed to be intentionally customized and left alone. Also offers to fix the executable bit independently when content is already current, since silently chmod'ing would otherwise show up as a confusing unprompted diff in git status. Promote permissions.py's EOF-safe y/n prompt helper to a public ask_yes_no() so this feature can reuse the same fail-safe behavior when stdin isn't interactive.
1 parent 63a7043 commit dc6dbd9

4 files changed

Lines changed: 127 additions & 8 deletions

File tree

Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,16 @@ RUN /venv/bin/python -m pip install -r /requirements.txt
2121
# Apply rdflib fixes
2222
RUN /venv/bin/python -m pip install git+https://github.com/avillar/rdflib.git@6.x
2323

24+
# Reference copy of bblock-template, used to detect outdated scaffolding scripts
25+
# (build.sh, view.sh, ...) in the repo being processed
26+
RUN git clone --depth 1 https://github.com/opengeospatial/bblocks-template.git /opt/bblocks-template && \
27+
rm -rf /opt/bblocks-template/.git
28+
2429
ENV PYTHONPATH=/src/
2530
ENV PYTHONUNBUFFERED=1
2631
ENV NODE_PATH=/src/node_modules
2732
ENV BBP_GIT_INFO_FILE=/GIT_INFO
33+
ENV BBP_TEMPLATE_DIR=/opt/bblocks-template
2834

2935
COPY ogc/ /src/ogc/
3036

ogc/bblocks/entrypoint.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from ogc.bblocks.log import setup_logging, log_indent
1515

1616
from ogc.bblocks.postprocess import postprocess
17+
from ogc.bblocks.template_sync import check_template_files
1718
from ogc.na import ingest_json, update_vocabs
1819

1920
import jsonschema
@@ -309,6 +310,9 @@
309310
except Exception as e:
310311
logger.warning("Could not autodetect base_url / github_base_url: %s", e)
311312

313+
if git_repo_path:
314+
check_template_files(git_repo_path)
315+
312316
steps = args.steps.split(',') if args.steps else None
313317

314318
# 1. Postprocess BBs

ogc/bblocks/permissions.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,14 @@ def _save_cache(sandbox_dir: Path, cache: dict) -> None:
3030
json.dump(cache, f, indent=2)
3131

3232

33-
def _ask_yes_no(prompt: str) -> bool:
33+
_DEFAULT_NO_INPUT_MESSAGE = (
34+
"No interactive input available to answer this prompt (stdin is closed) "
35+
"- denying by default. Run `docker run -i ...` to answer prompts interactively, "
36+
"or pass --skip-permissions true to bypass permission checks entirely."
37+
)
38+
39+
40+
def ask_yes_no(prompt: str, no_input_message: str = _DEFAULT_NO_INPUT_MESSAGE) -> bool:
3441
"""Ask a y/n question.
3542
3643
If stdin has no interactive input to offer (e.g. `docker run` without `-i`),
@@ -41,11 +48,7 @@ def _ask_yes_no(prompt: str) -> bool:
4148
try:
4249
answer = input(f"{prompt} [y/N] ").strip().lower()
4350
except EOFError:
44-
logger.warning(
45-
"No interactive input available to answer this prompt (stdin is closed) "
46-
"- denying by default. Run `docker run -i ...` to answer prompts interactively, "
47-
"or pass --skip-permissions true to bypass permission checks entirely."
48-
)
51+
logger.warning(no_input_message)
4952
return False
5053
if answer in ('y', 'yes'):
5154
return True
@@ -126,7 +129,7 @@ def _check_plugin_permissions(
126129
if pip_deps:
127130
print(f"║ Dependencies: {', '.join(pip_deps)}")
128131
print()
129-
if _ask_yes_no(f"Allow {label.lower()} plugin '{module}' to be installed and run?"):
132+
if ask_yes_no(f"Allow {label.lower()} plugin '{module}' to be installed and run?"):
130133
cached[module] = version_key
131134
allowed.add(module)
132135
cache[cache_key] = cached
@@ -169,7 +172,7 @@ def check_permissions(
169172
for bb_id, bb_name in unapproved_types[t_type]:
170173
print(f"║ • {bb_id} ({bb_name})")
171174
print("║")
172-
if _ask_yes_no("Allow these transforms to run?"):
175+
if ask_yes_no("Allow these transforms to run?"):
173176
for t_type in unapproved_types:
174177
cached_types.add(t_type)
175178
cache['transform-types'] = sorted(cached_types)

ogc/bblocks/template_sync.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
import os
5+
from pathlib import Path
6+
7+
from ogc.bblocks.permissions import ask_yes_no
8+
9+
logger = logging.getLogger(__name__)
10+
11+
_TEMPLATE_DIR_ENV = 'BBP_TEMPLATE_DIR'
12+
_TRACKED_FILES = ('build.sh', 'view.sh')
13+
_MAX_COMMITS_SCANNED = 20
14+
15+
16+
def _is_executable(path: Path) -> bool:
17+
return bool(path.stat().st_mode & 0o111)
18+
19+
20+
def _make_executable(path: Path) -> None:
21+
path.chmod(path.stat().st_mode | 0o755)
22+
23+
24+
def check_template_files(git_repo_path: Path) -> None:
25+
"""Offer to update scaffolding files (build.sh, view.sh, ...) that are
26+
outdated copies of their bblock-template counterparts.
27+
28+
A file is only treated as a candidate for updating if it has never been
29+
modified since it was added to the repo's git history - if it has, we
30+
assume it was intentionally customized and leave it alone.
31+
"""
32+
template_dir = os.environ.get(_TEMPLATE_DIR_ENV)
33+
if not template_dir:
34+
return
35+
template_dir = Path(template_dir)
36+
if not template_dir.is_dir():
37+
return
38+
39+
try:
40+
import git
41+
repo = git.Repo(git_repo_path)
42+
except Exception as e:
43+
logger.debug("Could not open git repo at %s to check template files: %s", git_repo_path, e)
44+
return
45+
46+
for filename in _TRACKED_FILES:
47+
target = git_repo_path / filename
48+
template = template_dir / filename
49+
if not target.is_file() or not template.is_file():
50+
continue
51+
52+
commits = list(repo.iter_commits(paths=filename, max_count=_MAX_COMMITS_SCANNED))
53+
if not commits:
54+
continue
55+
56+
# Commits that only touch the file's mode (e.g. `chmod a+x build.sh`)
57+
# leave its blob hash unchanged, so they don't count as customization
58+
content_hashes = set()
59+
for commit in commits:
60+
try:
61+
content_hashes.add(commit.tree[filename].hexsha)
62+
except KeyError:
63+
pass
64+
if len(content_hashes) != 1:
65+
# Content actually changed across commits (or couldn't be read),
66+
# so we can't be sure it's still the pristine template version
67+
logger.debug(
68+
"Skipping template check for %s: its content changed across %d commit(s) "
69+
"(expected its content to be unchanged since it was added)", filename, len(commits),
70+
)
71+
continue
72+
73+
if target.read_bytes() != template.read_bytes():
74+
print()
75+
print("╔══ Outdated template file detected")
76+
print(f"║ {filename} differs from the latest version in bblocks-template,")
77+
print(f"║ and does not appear to have been customized.")
78+
print("║")
79+
if ask_yes_no(
80+
f"Update {filename} to the latest bblocks-template version?",
81+
no_input_message=(
82+
f"No interactive input available to ask about updating {filename} "
83+
f"(stdin is closed) - leaving it as-is. It may be outdated; compare "
84+
f"against https://github.com/opengeospatial/bblocks-template/blob/master/{filename}"
85+
),
86+
):
87+
target.write_bytes(template.read_bytes())
88+
_make_executable(target)
89+
print(f" Updated {filename}.")
90+
# The executable bit was implicitly accepted along with the content update
91+
continue
92+
93+
if not _is_executable(target):
94+
print()
95+
print("╔══ Template file is not executable")
96+
print(f"║ {filename} is missing the executable bit.")
97+
print("║")
98+
if ask_yes_no(
99+
f"Make {filename} executable?",
100+
no_input_message=(
101+
f"No interactive input available to ask about making {filename} executable "
102+
f"(stdin is closed) - leaving it as-is."
103+
),
104+
):
105+
_make_executable(target)
106+
print(f" Made {filename} executable.")

0 commit comments

Comments
 (0)