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
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>
44 lines
2.0 KiB
Python
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)')
|