From d296cba4361d932d821d1b76e862e7bcbbcc2fd6 Mon Sep 17 00:00:00 2001 From: Julien Denizot Date: Tue, 23 Jun 2026 00:13:27 +0200 Subject: [PATCH] build(v16): lock multi-arch r4 inputs --- images/Containerfile.v16-prod | 80 ++++++++---- manifests/base-images.v16.prod.lock.json | 10 ++ scripts/clone-locked-apps.py | 160 +++++++++++++++++++++++ 3 files changed, 226 insertions(+), 24 deletions(-) create mode 100644 manifests/base-images.v16.prod.lock.json create mode 100755 scripts/clone-locked-apps.py diff --git a/images/Containerfile.v16-prod b/images/Containerfile.v16-prod index 2a9b246..6d05528 100644 --- a/images/Containerfile.v16-prod +++ b/images/Containerfile.v16-prod @@ -1,35 +1,51 @@ -ARG FRAPPE_BRANCH=version-16 -ARG FRAPPE_IMAGE_PREFIX=frappe +ARG FRAPPE_BRANCH=v16.23.1 +ARG FRAPPE_BUILD_IMAGE=frappe/build:version-16@sha256:ca022851cd8babd508e76ed36fc61d60776d2c3b938d869cf12eb6df16716e58 +ARG FRAPPE_BASE_IMAGE=frappe/base:version-16@sha256:0a43692eeb9c2f18674269ece2811ffb0f0543556fc701b77727aeeb587bb0e0 -FROM ${FRAPPE_IMAGE_PREFIX}/build:${FRAPPE_BRANCH} AS builder +FROM ${FRAPPE_BUILD_IMAGE} AS builder -ARG FRAPPE_BRANCH=version-16 +ARG FRAPPE_BRANCH=v16.23.1 ARG FRAPPE_PATH=https://github.com/frappe/frappe ARG CACHE_BUST="" +COPY --chown=frappe:frappe \ + manifests/apps.v16.prod.lock.json \ + /opt/enuxia/apps.v16.prod.lock.json + +COPY --chown=frappe:frappe \ + scripts/clone-locked-apps.py \ + /opt/enuxia/clone-locked-apps.py + COPY --chown=frappe:frappe \ patches/crm/event.py \ /opt/enuxia-patches/crm/event.py USER frappe -RUN --mount=type=secret,id=apps_json,target=/opt/frappe/apps.json,uid=1000,gid=1000 \ - set -eux; \ +RUN set -eux; \ : "${CACHE_BUST}"; \ - APP_INSTALL_ARGS=""; \ - if [ -s /opt/frappe/apps.json ]; then \ - APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \ - fi; \ - bench init ${APP_INSTALL_ARGS} \ + bench init \ --frappe-branch="${FRAPPE_BRANCH}" \ --frappe-path="${FRAPPE_PATH}" \ --no-procfile \ --no-backups \ --skip-redis-config-generation \ + --skip-assets \ --verbose \ /home/frappe/frappe-bench; \ cd /home/frappe/frappe-bench; \ - echo "{}" > sites/common_site_config.json; \ + printf '%s\n' '{}' > sites/common_site_config.json + +RUN set -eux; \ + python3 /opt/enuxia/clone-locked-apps.py \ + /opt/enuxia/apps.v16.prod.lock.json \ + /home/frappe/frappe-bench; \ + cd /home/frappe/frappe-bench; \ + cp /opt/enuxia/apps.v16.prod.lock.json enuxia-build-lock.json; \ + bench setup requirements + +RUN set -eux; \ + cd /home/frappe/frappe-bench; \ \ echo "=== Correctif LMS ==="; \ LMS_SOURCE="apps/lms/lms/lms/patches/v2_0/add_video_watch_duration_index.py"; \ @@ -64,14 +80,22 @@ RUN --mount=type=secret,id=apps_json,target=/opt/frappe/apps.json,uid=1000,gid=1 "${CRM_TARGET}" \ "${GAMEPLAN_FILE}"; \ \ - rm -rf /opt/enuxia-patches; \ - find apps -mindepth 1 -path "*/.git" -prune -exec rm -rf {} + + bench set-config -gp socketio_port 9000; \ + bench build --production; \ + bench version > enuxia-build-versions.txt; \ + cp sites/apps.txt enuxia-build-apps.txt; \ + find apps \ + -mindepth 2 \ + -type d \ + -name .git \ + -prune \ + -exec rm -rf {} + -FROM ${FRAPPE_IMAGE_PREFIX}/base:${FRAPPE_BRANCH} AS backend +FROM ${FRAPPE_BASE_IMAGE} AS backend LABEL org.opencontainers.image.title="Enuxia Suite" -LABEL org.opencontainers.image.description="Production Frappe and ERPNext v16 image maintained by Enuxia" -LABEL org.opencontainers.image.version="16.23.1-r3" +LABEL org.opencontainers.image.description="Production Frappe and ERPNext v16 multi-architecture image maintained by Enuxia" +LABEL org.opencontainers.image.version="16.23.1-r4" LABEL org.opencontainers.image.source="https://git.enuxia.fr/Enuxia-Public/enuxia-suite" USER frappe @@ -87,19 +111,27 @@ RUN cp -r \ /home/frappe/frappe-bench/assets && \ rm -rf /home/frappe/frappe-bench/sites/assets -VOLUME [ \ - "/home/frappe/frappe-bench/sites", \ - "/home/frappe/frappe-bench/logs" \ -] +VOLUME ["/home/frappe/frappe-bench/sites", "/home/frappe/frappe-bench/logs"] USER root +RUN mkdir -p /opt/enuxia/manifests + +COPY manifests/apps.v16.prod.lock.json \ + /opt/enuxia/manifests/apps.v16.prod.lock.json + +COPY manifests/base-images.v16.prod.lock.json \ + /opt/enuxia/manifests/base-images.v16.prod.lock.json + COPY resources/core/main-entrypoint.sh /usr/local/bin/entrypoint.sh COPY resources/core/start.sh /usr/local/bin/start.sh -RUN chmod 755 \ - /usr/local/bin/entrypoint.sh \ - /usr/local/bin/start.sh +RUN chmod 0444 \ + /opt/enuxia/manifests/apps.v16.prod.lock.json \ + /opt/enuxia/manifests/base-images.v16.prod.lock.json && \ + chmod 0755 \ + /usr/local/bin/entrypoint.sh \ + /usr/local/bin/start.sh USER frappe diff --git a/manifests/base-images.v16.prod.lock.json b/manifests/base-images.v16.prod.lock.json new file mode 100644 index 0000000..35d184c --- /dev/null +++ b/manifests/base-images.v16.prod.lock.json @@ -0,0 +1,10 @@ +{ + "build": { + "reference": "frappe/build:version-16", + "digest": "sha256:ca022851cd8babd508e76ed36fc61d60776d2c3b938d869cf12eb6df16716e58" + }, + "base": { + "reference": "frappe/base:version-16", + "digest": "sha256:0a43692eeb9c2f18674269ece2811ffb0f0543556fc701b77727aeeb587bb0e0" + } +} diff --git a/scripts/clone-locked-apps.py b/scripts/clone-locked-apps.py new file mode 100755 index 0000000..9606062 --- /dev/null +++ b/scripts/clone-locked-apps.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 + +import argparse +import json +import re +import subprocess +from pathlib import Path + +COMMIT_RE = re.compile(r"^[0-9a-f]{40}$") + + +def command(args, cwd=None, capture=False): + result = subprocess.run( + args, + cwd=cwd, + check=True, + text=True, + stdout=subprocess.PIPE if capture else None, + ) + return result.stdout.strip() if capture else "" + + +def load_lock(path): + data = json.loads(path.read_text(encoding="utf-8")) + + if not isinstance(data, dict): + raise SystemExit("Le verrou doit être un objet JSON.") + + if "frappe" not in data: + raise SystemExit("Entrée frappe absente du verrou.") + + entries = [] + + for name, metadata in data.items(): + if not isinstance(metadata, dict): + raise SystemExit(f"Métadonnées invalides pour {name}.") + + resolution = metadata.get("resolution") or {} + url = metadata.get("url") + branch = resolution.get("branch") + commit = resolution.get("commit_hash") + idx = metadata.get("idx") + + if not isinstance(url, str) or not url.startswith("https://"): + raise SystemExit(f"URL publique invalide pour {name}: {url!r}") + + if not isinstance(branch, str) or not branch: + raise SystemExit(f"Branche absente pour {name}.") + + if not isinstance(commit, str) or not COMMIT_RE.fullmatch(commit): + raise SystemExit(f"Commit invalide pour {name}: {commit!r}") + + if not isinstance(idx, int): + raise SystemExit(f"Index invalide pour {name}: {idx!r}") + + entries.append((name, metadata)) + + entries.sort(key=lambda item: item[1]["idx"]) + + expected = list(range(1, len(entries) + 1)) + actual = [metadata["idx"] for _, metadata in entries] + + if actual != expected: + raise SystemExit(f"Indices non continus dans le verrou: {actual!r}") + + return entries + + +def verify_head(repository, expected_commit, app_name): + actual = command( + ["git", "rev-parse", "HEAD"], + cwd=repository, + capture=True, + ) + + if actual != expected_commit: + raise SystemExit( + f"Commit incorrect pour {app_name}: " + f"attendu={expected_commit} obtenu={actual}" + ) + + +def main(): + parser = argparse.ArgumentParser( + description="Clone les applications Frappe aux commits du verrou." + ) + parser.add_argument("lock_file", type=Path) + parser.add_argument("bench_path", type=Path) + parser.add_argument("--validate-only", action="store_true") + args = parser.parse_args() + + entries = load_lock(args.lock_file) + app_entries = [ + (name, metadata) + for name, metadata in entries + if name != "frappe" + ] + + if args.validate_only: + print( + f"Verrou valide: {len(entries)} entrées, " + f"{len(app_entries)} applications." + ) + return + + bench = args.bench_path.resolve() + apps_dir = bench / "apps" + sites_dir = bench / "sites" + + frappe_metadata = dict(entries)["frappe"] + verify_head( + apps_dir / "frappe", + frappe_metadata["resolution"]["commit_hash"], + "frappe", + ) + + for name, metadata in app_entries: + target = apps_dir / name + + if target.exists(): + raise SystemExit(f"Répertoire déjà présent: {target}") + + resolution = metadata["resolution"] + branch = resolution["branch"] + commit = resolution["commit_hash"] + url = metadata["url"] + + print(f"Clonage de {name} @ {commit}") + + command( + [ + "git", + "clone", + "--filter=blob:none", + "--no-checkout", + "--single-branch", + "--branch", + branch, + url, + str(target), + ] + ) + command( + ["git", "checkout", "--detach", commit], + cwd=target, + ) + verify_head(target, commit, name) + + app_names = [name for name, _ in entries] + (sites_dir / "apps.txt").write_text( + "\n".join(app_names) + "\n", + encoding="utf-8", + ) + + print(f"Applications verrouillées clonées: {len(app_entries)}") + print("Frappe et applications vérifiés: OK") + + +if __name__ == "__main__": + main()