Files
dtf-system/infra/compile_web.py
Cauê Faleiros 536510b148
All checks were successful
Build and deploy / Validate source (push) Successful in 7s
Build and deploy / Integration suite on a real stack (push) Successful in 2m9s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 1m44s
fix: version the Site's scripts by content so a release never meets a cached old one
The proxy in front of production caches .js and .css for hours. After the
last release the Site got the new index.html with the old site-flow.js,
which wrote to an element the new page no longer has; the error left
"Adicionar ao carrinho" disabled. The web build now addresses every local
script and stylesheet by a hash of its content, replacing the hand-kept
?v= markers, so a new release always loads its own files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:27:55 -03:00

44 lines
2.0 KiB
Python

"""Compile the gateway configuration and version the Site's assets.
Inline scripts are hashed for the CSP; local scripts and stylesheets get a
content hash in their address.
The Site's behaviour now lives in separate files, so normally there is nothing to
hash and the policy is simply script-src 'self' — no allowlist to get wrong. The
hashing stays because an inline script added later must not silently need
'unsafe-inline'; it is hashed automatically instead.
"""
import base64
import hashlib
import os
from pathlib import Path
import re
root = Path('/build')
# Every local script and stylesheet is addressed by its content, so a release
# can never pair new HTML with an old cached file: the proxy in front of the
# stack caches assets for hours, and a new index.html calling an old script
# broke the Site (2026-09-28). The HTML itself is served no-cache.
def versioned(match):
attribute, path = match.group(1), match.group(2)
digest = hashlib.sha256((root / 'web' / path.lstrip('/')).read_bytes()).hexdigest()[:12]
return f'{attribute}="{path}?v={digest}"'
for html in (root / 'web').glob('*.html'):
text = re.sub(r'\b(src|href)="(/[\w./-]+\.(?:js|css))(?:\?[^"]*)?"', versioned, html.read_text())
html.write_text(text)
hashes = []
for html in (root / 'web').glob('*.html'):
for attributes, script in re.findall(r'<script\b([^>]*)>(.*?)</script>', html.read_text(), re.S | re.I):
if not re.search(r'\bsrc\s*=', attributes, re.I) and script.strip():
hashes.append("'sha256-" + base64.b64encode(hashlib.sha256(script.encode()).digest()).decode() + "'")
template = Path(os.environ.get('NGINX_TEMPLATE', root / 'infra/nginx.conf.template')).read_text()
rendered = template.replace('@SCRIPT_HASHES@', ' '.join(hashes))
# Collapse the gap an empty hash list leaves behind, so the policy reads cleanly.
rendered = re.sub(r"(script-src 'self')\s+;", r'\1;', rendered)
(root / 'default.conf.template').write_text(rendered)
print(f'CSP script-src: {len(hashes)} inline hash(es)')