build(v16): lock multi-arch r4 inputs

This commit is contained in:
Julien Denizot
2026-06-23 00:13:27 +02:00
parent 7807f97fb3
commit d296cba436
3 changed files with 226 additions and 24 deletions
+56 -24
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
{
"build": {
"reference": "frappe/build:version-16",
"digest": "sha256:ca022851cd8babd508e76ed36fc61d60776d2c3b938d869cf12eb6df16716e58"
},
"base": {
"reference": "frappe/base:version-16",
"digest": "sha256:0a43692eeb9c2f18674269ece2811ffb0f0543556fc701b77727aeeb587bb0e0"
}
}
+160
View File
@@ -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()