mirror of
https://git.hexahost.dev/smueller/HexaHost-Frontend.git
synced 2026-06-02 10:28:43 +00:00
Compare commits
2 Commits
6c9114e0a7
...
e9d5b55459
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d5b55459 | ||
|
|
8f985da61f |
@@ -1,9 +1,9 @@
|
|||||||
name: Obfuscate Main Build
|
name: Release Build (dev → main)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- dev
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -11,18 +11,34 @@ env:
|
|||||||
REPO_PATH: smueller/HexaHost-Frontend
|
REPO_PATH: smueller/HexaHost-Frontend
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
obfuscate:
|
release-build:
|
||||||
# Kein erneuter Lauf nach dem Bot-Commit
|
|
||||||
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
|
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout (volle History)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# Gitea liefert intern oft eine IP; Zertifikat gilt für git.hexahost.dev
|
|
||||||
repository-url: https://git.hexahost.dev/smueller/HexaHost-Frontend
|
repository-url: https://git.hexahost.dev/smueller/HexaHost-Frontend
|
||||||
|
ref: dev
|
||||||
|
|
||||||
|
- name: Merge dev in CI-Workspace (Basis main)
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
git config user.name "gitea-actions"
|
||||||
|
git config user.email "actions@local"
|
||||||
|
git remote set-url origin "https://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH}.git"
|
||||||
|
git fetch origin main dev
|
||||||
|
|
||||||
|
if git show-ref --verify --quiet refs/remotes/origin/main; then
|
||||||
|
git checkout -B main origin/main
|
||||||
|
git merge origin/dev -X theirs --no-edit -m "ci: merge dev for release build"
|
||||||
|
else
|
||||||
|
echo "main branch missing, initializing from dev"
|
||||||
|
git checkout -B main origin/dev
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
@@ -37,7 +53,7 @@ jobs:
|
|||||||
- name: Run release obfuscation
|
- name: Run release obfuscation
|
||||||
run: python scripts/obfuscate_release.py --root . --hash-assets
|
run: python scripts/obfuscate_release.py --root . --hash-assets
|
||||||
|
|
||||||
- name: Commit obfuscated build
|
- name: Publish release to main
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ github.token }}
|
GITEA_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
@@ -46,8 +62,8 @@ jobs:
|
|||||||
git remote set-url origin "https://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH}.git"
|
git remote set-url origin "https://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH}.git"
|
||||||
git add -A
|
git add -A
|
||||||
if git diff --cached --quiet; then
|
if git diff --cached --quiet; then
|
||||||
echo "No build changes to commit."
|
echo "No release changes to publish."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
git commit -m "chore(release): obfuscate and hash production assets [skip ci]"
|
git commit -m "chore(release): obfuscate and hash production assets [skip ci]"
|
||||||
git push origin HEAD:main
|
git push origin main
|
||||||
|
|||||||
50
.github/workflows/obfuscate-main.yml
vendored
50
.github/workflows/obfuscate-main.yml
vendored
@@ -1,44 +1,70 @@
|
|||||||
name: Obfuscate Main Build
|
# Hinweis: Gitea nutzt .gitea/workflows/obfuscate-main.yml (identischer Ablauf).
|
||||||
|
name: Release Build (dev → main)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- dev
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
env:
|
||||||
contents: write
|
GITEA_HOST: git.hexahost.dev
|
||||||
|
REPO_PATH: smueller/HexaHost-Frontend
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
obfuscate:
|
release-build:
|
||||||
if: github.actor != 'github-actions[bot]'
|
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout (volle History)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
repository-url: https://git.hexahost.dev/smueller/HexaHost-Frontend
|
||||||
|
ref: dev
|
||||||
|
|
||||||
|
- name: Merge dev in CI-Workspace (Basis main)
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
git config user.name "gitea-actions"
|
||||||
|
git config user.email "actions@local"
|
||||||
|
git remote set-url origin "https://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH}.git"
|
||||||
|
git fetch origin main dev
|
||||||
|
|
||||||
|
if git show-ref --verify --quiet refs/remotes/origin/main; then
|
||||||
|
git checkout -B main origin/main
|
||||||
|
git merge origin/dev -X theirs --no-edit -m "ci: merge dev for release build"
|
||||||
|
else
|
||||||
|
echo "main branch missing, initializing from dev"
|
||||||
|
git checkout -B main origin/dev
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: "20"
|
||||||
|
|
||||||
- name: Run release obfuscation
|
- name: Run release obfuscation
|
||||||
run: python scripts/obfuscate_release.py --root . --hash-assets
|
run: python scripts/obfuscate_release.py --root . --hash-assets
|
||||||
|
|
||||||
- name: Commit obfuscated build
|
- name: Publish release to main
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
|
git config user.name "gitea-actions"
|
||||||
|
git config user.email "actions@local"
|
||||||
|
git remote set-url origin "https://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH}.git"
|
||||||
git add -A
|
git add -A
|
||||||
if git diff --cached --quiet; then
|
if git diff --cached --quiet; then
|
||||||
echo "No build changes to commit."
|
echo "No release changes to publish."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
git commit -m "chore(release): obfuscate and hash production assets [skip ci]"
|
git commit -m "chore(release): obfuscate and hash production assets [skip ci]"
|
||||||
git push
|
git push origin main
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -166,9 +166,21 @@ Für den Produktivbetrieb `public/` als Webroot konfigurieren.
|
|||||||
|
|
||||||
### Production-Build & Veröffentlichung
|
### Production-Build & Veröffentlichung
|
||||||
|
|
||||||
Der Quellcode bleibt auf `dev`, der veröffentlichte Stand liegt auf `main`.
|
| Branch | Zweck |
|
||||||
|
|--------|--------|
|
||||||
|
| **`dev`** | Entwicklung (lesbarer Code, Kommentare) |
|
||||||
|
| **`main`** | Release/Produktion (obfuskiert, gehashte Assets) |
|
||||||
|
|
||||||
Bei jedem Push/Merge auf `main` läuft die GitHub Action `.github/workflows/obfuscate-main.yml` automatisch und führt aus:
|
**Workflow:** Nur auf `dev` entwickeln und pushen — **nicht** `dev` manuell nach `main` mergen.
|
||||||
|
|
||||||
|
Bei jedem Push auf `dev` startet `.gitea/workflows/obfuscate-main.yml`:
|
||||||
|
|
||||||
|
1. Checkout in temporärem Runner-Workspace
|
||||||
|
2. `dev` in CI mit `main` mergen (`-X theirs`, dev-Inhalte bei Konflikten)
|
||||||
|
3. Obfuscation-Build (`scripts/obfuscate_release.py --hash-assets`)
|
||||||
|
4. Ergebnis nach `main` pushen (Bot-Commit mit `[skip ci]`)
|
||||||
|
|
||||||
|
Der Build führt aus:
|
||||||
|
|
||||||
- Entfernen von Kommentaren (inkl. Block-Kommentaren) in PHP/JS/CSS
|
- Entfernen von Kommentaren (inkl. Block-Kommentaren) in PHP/JS/CSS
|
||||||
- Minify + Obfuscate für JavaScript
|
- Minify + Obfuscate für JavaScript
|
||||||
@@ -176,7 +188,7 @@ Bei jedem Push/Merge auf `main` läuft die GitHub Action `.github/workflows/obfu
|
|||||||
- Kein Source-Map-Output
|
- Kein Source-Map-Output
|
||||||
- Hashing von JS/CSS-Dateinamen + automatische Referenz-Anpassung
|
- Hashing von JS/CSS-Dateinamen + automatische Referenz-Anpassung
|
||||||
|
|
||||||
Lokal ausführbar:
|
Lokal testen (nur in Kopie, nicht committen):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python scripts/obfuscate_release.py --root . --hash-assets
|
python scripts/obfuscate_release.py --root . --hash-assets
|
||||||
|
|||||||
Binary file not shown.
@@ -3,15 +3,17 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
TEXT_EXTENSIONS = {".php", ".html", ".htm", ".xml", ".txt", ".js", ".css"}
|
TEXT_EXTENSIONS = {".php", ".html", ".htm", ".xml", ".txt", ".js", ".css"}
|
||||||
|
HASH_SUFFIX_RE = re.compile(r"\.[a-f0-9]{12}$", re.I)
|
||||||
|
|
||||||
|
|
||||||
def strip_comments_keep_strings(text: str) -> str:
|
def strip_comments_keep_strings(text: str) -> str:
|
||||||
@@ -174,42 +176,124 @@ def minify_js_fallback(text: str) -> str:
|
|||||||
return text.strip()
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
def run_cmd(command: list[str], cwd: Path, input_text: str | None = None) -> str:
|
def canonical_asset_base(stem: str) -> str:
|
||||||
|
name = stem
|
||||||
|
while HASH_SUFFIX_RE.search(name):
|
||||||
|
name = HASH_SUFFIX_RE.sub("", name)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def is_skipped_asset(path: Path) -> bool:
|
||||||
|
lowered = path.as_posix().lower()
|
||||||
|
if ".min." in path.name or ".obf." in path.name or ".deob." in path.name:
|
||||||
|
return True
|
||||||
|
if "deobfuscated" in lowered:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_source_content(content: str) -> bool:
|
||||||
|
if "[javascript-obfuscator-cli]" in content:
|
||||||
|
return False
|
||||||
|
return len(content.strip()) >= 20
|
||||||
|
|
||||||
|
|
||||||
|
def collect_asset_groups(asset_root: Path) -> dict[tuple[Path, str, str], list[Path]]:
|
||||||
|
groups: dict[tuple[Path, str, str], list[Path]] = defaultdict(list)
|
||||||
|
for ext in (".js", ".css"):
|
||||||
|
for file_path in sorted(asset_root.rglob(f"*{ext}")):
|
||||||
|
if is_skipped_asset(file_path):
|
||||||
|
continue
|
||||||
|
base = canonical_asset_base(file_path.stem)
|
||||||
|
key = (file_path.parent, base, ext)
|
||||||
|
groups[key].append(file_path)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
def pick_source_file(paths: list[Path], base: str, ext: str) -> Path | None:
|
||||||
|
if not paths:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parent = paths[0].parent
|
||||||
|
plain = parent / f"{base}{ext}"
|
||||||
|
ordered: list[Path] = []
|
||||||
|
if plain in paths:
|
||||||
|
ordered.append(plain)
|
||||||
|
for candidate in sorted(paths, key=lambda p: len(p.name)):
|
||||||
|
if candidate not in ordered:
|
||||||
|
ordered.append(candidate)
|
||||||
|
|
||||||
|
for candidate in ordered:
|
||||||
|
try:
|
||||||
|
content = candidate.read_text(encoding="utf-8")
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
continue
|
||||||
|
if is_valid_source_content(content):
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_invalid_siblings(paths: list[Path], source: Path) -> None:
|
||||||
|
for path in paths:
|
||||||
|
if path == source or not path.exists():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
content = path.read_text(encoding="utf-8")
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
content = ""
|
||||||
|
if not is_valid_source_content(content):
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd(command: list[str], cwd: Path) -> None:
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
command,
|
command,
|
||||||
cwd=str(cwd),
|
cwd=str(cwd),
|
||||||
input=input_text,
|
|
||||||
text=True,
|
text=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
raise RuntimeError(proc.stderr.strip() or "command failed")
|
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "command failed")
|
||||||
return proc.stdout
|
|
||||||
|
|
||||||
|
|
||||||
def process_js(path: Path, cwd: Path) -> None:
|
def process_js(path: Path, cwd: Path) -> None:
|
||||||
original = path.read_text(encoding="utf-8")
|
original = path.read_text(encoding="utf-8")
|
||||||
|
if not is_valid_source_content(original):
|
||||||
|
raise ValueError(
|
||||||
|
f"invalid or corrupted JS source: {path} "
|
||||||
|
f"(restore e.g. 'git checkout dev -- {path.as_posix()}')"
|
||||||
|
)
|
||||||
|
|
||||||
if shutil.which("npx"):
|
if shutil.which("npx"):
|
||||||
|
tmpdir = Path(tempfile.mkdtemp())
|
||||||
try:
|
try:
|
||||||
minified = run_cmd(
|
src = tmpdir / "input.js"
|
||||||
|
out = tmpdir / "output.js"
|
||||||
|
src.write_text(original, encoding="utf-8")
|
||||||
|
run_cmd(
|
||||||
[
|
[
|
||||||
"npx",
|
"npx",
|
||||||
"--yes",
|
"--yes",
|
||||||
"terser",
|
"terser",
|
||||||
|
str(src),
|
||||||
|
"-o",
|
||||||
|
str(src),
|
||||||
"--compress",
|
"--compress",
|
||||||
"--mangle",
|
"--mangle",
|
||||||
"--comments",
|
"--comments",
|
||||||
"false",
|
"false",
|
||||||
],
|
],
|
||||||
cwd,
|
cwd,
|
||||||
input_text=original,
|
|
||||||
)
|
)
|
||||||
obfuscated = run_cmd(
|
run_cmd(
|
||||||
[
|
[
|
||||||
"npx",
|
"npx",
|
||||||
"--yes",
|
"--yes",
|
||||||
"javascript-obfuscator",
|
"javascript-obfuscator",
|
||||||
|
str(src),
|
||||||
|
"--output",
|
||||||
|
str(out),
|
||||||
"--compact",
|
"--compact",
|
||||||
"true",
|
"true",
|
||||||
"--control-flow-flattening",
|
"--control-flow-flattening",
|
||||||
@@ -224,38 +308,51 @@ def process_js(path: Path, cwd: Path) -> None:
|
|||||||
"browser-no-eval",
|
"browser-no-eval",
|
||||||
"--source-map",
|
"--source-map",
|
||||||
"false",
|
"false",
|
||||||
"--output",
|
|
||||||
"stdout",
|
|
||||||
],
|
],
|
||||||
cwd,
|
cwd,
|
||||||
input_text=minified,
|
|
||||||
)
|
)
|
||||||
path.write_text(obfuscated.strip() + "\n", encoding="utf-8")
|
if not out.exists():
|
||||||
|
raise RuntimeError("obfuscator produced no output file")
|
||||||
|
result = out.read_text(encoding="utf-8")
|
||||||
|
if not is_valid_source_content(result):
|
||||||
|
raise RuntimeError("obfuscator output looks invalid")
|
||||||
|
path.write_text(result.strip() + "\n", encoding="utf-8")
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||||
|
|
||||||
path.write_text(minify_js_fallback(original) + "\n", encoding="utf-8")
|
path.write_text(minify_js_fallback(original) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def process_css(path: Path, cwd: Path) -> None:
|
def process_css(path: Path, cwd: Path) -> None:
|
||||||
original = path.read_text(encoding="utf-8")
|
original = path.read_text(encoding="utf-8")
|
||||||
if shutil.which("npx"):
|
if shutil.which("npx"):
|
||||||
|
tmpdir = Path(tempfile.mkdtemp())
|
||||||
try:
|
try:
|
||||||
minified = run_cmd(
|
src = tmpdir / "input.css"
|
||||||
|
out = tmpdir / "output.css"
|
||||||
|
src.write_text(original, encoding="utf-8")
|
||||||
|
run_cmd(
|
||||||
[
|
[
|
||||||
"npx",
|
"npx",
|
||||||
"--yes",
|
"--yes",
|
||||||
"clean-css-cli",
|
"clean-css-cli",
|
||||||
|
str(src),
|
||||||
|
"-o",
|
||||||
|
str(out),
|
||||||
"--skip-rebase",
|
"--skip-rebase",
|
||||||
"-O2",
|
"-O2",
|
||||||
],
|
],
|
||||||
cwd,
|
cwd,
|
||||||
input_text=original,
|
|
||||||
)
|
)
|
||||||
path.write_text(minified.strip() + "\n", encoding="utf-8")
|
path.write_text(out.read_text(encoding="utf-8").strip() + "\n", encoding="utf-8")
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||||
path.write_text(minify_css_fallback(original) + "\n", encoding="utf-8")
|
path.write_text(minify_css_fallback(original) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
@@ -266,8 +363,7 @@ def process_php(path: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def hash_file(path: Path) -> str:
|
def hash_file(path: Path) -> str:
|
||||||
digest = hashlib.sha256(path.read_bytes()).hexdigest()[:12]
|
return hashlib.sha256(path.read_bytes()).hexdigest()[:12]
|
||||||
return digest
|
|
||||||
|
|
||||||
|
|
||||||
def replace_references(root: Path, mapping: dict[str, str]) -> None:
|
def replace_references(root: Path, mapping: dict[str, str]) -> None:
|
||||||
@@ -279,7 +375,7 @@ def replace_references(root: Path, mapping: dict[str, str]) -> None:
|
|||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
continue
|
continue
|
||||||
updated = content
|
updated = content
|
||||||
for src, dst in mapping.items():
|
for src, dst in sorted(mapping.items(), key=lambda item: len(item[0]), reverse=True):
|
||||||
updated = updated.replace(src, dst)
|
updated = updated.replace(src, dst)
|
||||||
updated = updated.replace("/" + src, "/" + dst)
|
updated = updated.replace("/" + src, "/" + dst)
|
||||||
if updated != content:
|
if updated != content:
|
||||||
@@ -289,17 +385,32 @@ def replace_references(root: Path, mapping: dict[str, str]) -> None:
|
|||||||
def build_hash_mapping(public_root: Path) -> dict[str, str]:
|
def build_hash_mapping(public_root: Path) -> dict[str, str]:
|
||||||
mapping: dict[str, str] = {}
|
mapping: dict[str, str] = {}
|
||||||
asset_root = public_root / "assets"
|
asset_root = public_root / "assets"
|
||||||
for ext in (".js", ".css"):
|
if not asset_root.exists():
|
||||||
for file_path in sorted(asset_root.rglob(f"*{ext}")):
|
return mapping
|
||||||
if ".min." in file_path.name or ".obf." in file_path.name:
|
|
||||||
|
groups = collect_asset_groups(asset_root)
|
||||||
|
for (parent, base, ext), paths in groups.items():
|
||||||
|
source = pick_source_file(paths, base, ext)
|
||||||
|
if source is None:
|
||||||
continue
|
continue
|
||||||
digest = hash_file(file_path)
|
digest = hash_file(source)
|
||||||
new_name = f"{file_path.stem}.{digest}{file_path.suffix}"
|
target = parent / f"{base}.{digest}{ext}"
|
||||||
new_path = file_path.with_name(new_name)
|
rel_new = target.relative_to(public_root).as_posix()
|
||||||
file_path.rename(new_path)
|
|
||||||
rel_old = file_path.relative_to(public_root).as_posix()
|
for old in paths:
|
||||||
rel_new = new_path.relative_to(public_root).as_posix()
|
rel_old = old.relative_to(public_root).as_posix()
|
||||||
|
if rel_old != rel_new:
|
||||||
mapping[rel_old] = rel_new
|
mapping[rel_old] = rel_new
|
||||||
|
|
||||||
|
if source != target:
|
||||||
|
if target.exists():
|
||||||
|
target.unlink()
|
||||||
|
source.replace(target)
|
||||||
|
|
||||||
|
for old in paths:
|
||||||
|
if old != target and old.exists():
|
||||||
|
old.unlink()
|
||||||
|
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
@@ -316,11 +427,26 @@ def main() -> int:
|
|||||||
print("public directory not found", file=sys.stderr)
|
print("public directory not found", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
for js in sorted(public_root.rglob("*.js")):
|
asset_root = public_root / "assets"
|
||||||
process_js(js, repo_root)
|
if asset_root.exists():
|
||||||
for css in sorted(public_root.rglob("*.css")):
|
groups = collect_asset_groups(asset_root)
|
||||||
process_css(css, repo_root)
|
for (parent, base, ext), paths in sorted(groups.items()):
|
||||||
for php in sorted((repo_root / "public").rglob("*.php")):
|
source = pick_source_file(paths, base, ext)
|
||||||
|
if source is None:
|
||||||
|
rel = (parent / f"{base}{ext}").relative_to(public_root)
|
||||||
|
print(
|
||||||
|
f"ERROR: No valid source for {rel}. "
|
||||||
|
f"Restore from dev, e.g.: git checkout dev -- {rel}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
cleanup_invalid_siblings(paths, source)
|
||||||
|
if ext == ".js":
|
||||||
|
process_js(source, repo_root)
|
||||||
|
else:
|
||||||
|
process_css(source, repo_root)
|
||||||
|
|
||||||
|
for php in sorted(public_root.rglob("*.php")):
|
||||||
process_php(php)
|
process_php(php)
|
||||||
for php in sorted((repo_root / "backend").rglob("*.php")):
|
for php in sorted((repo_root / "backend").rglob("*.php")):
|
||||||
process_php(php)
|
process_php(php)
|
||||||
|
|||||||
Reference in New Issue
Block a user