Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ git -C "$ROOT_DIR" add \
README.zh-CN.md \
CHANGELOG.md \
skills/zh/header.md.template \
skills/en/header.md.template
skills/en/header.md.template \
tests/golden-snapshots.json

cat >"$STATE_FILE" <<EOF
RELEASE_VERSION=$RELEASE_VERSION
Expand Down
91 changes: 91 additions & 0 deletions scripts/regenerate-golden-snapshots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Regenerate tests/golden-snapshots.json from current source files.

Golden snapshots detect unintended changes to installer output.
This script is the single source of truth for regeneration, called by:
- release-sync.sh (after version bump)
- release-preflight.sh (auto-fix during pre-commit)
"""

from __future__ import annotations

import hashlib
import json
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))

try:
from installer.hosts.base import HEADER_TEMPLATE_NAME, render_single_file # noqa: E402
from installer.hosts.claude import CLAUDE_ADAPTER # noqa: E402
from installer.hosts.codex import CODEX_ADAPTER # noqa: E402
from installer.hosts.copilot import COPILOT_ADAPTER # noqa: E402
from installer.models import language_to_source_dir # noqa: E402
except ImportError:
# Gracefully skip when run outside the full repo (e.g. test fixtures).
sys.exit(0)

GOLDEN = ROOT / "tests/golden-snapshots.json"
IGNORE = {".DS_Store", "Thumbs.db", "__pycache__"}

# Each adapter produces a different header file; hash = sha256(filename + content)
ADAPTERS = [
(CODEX_ADAPTER, "CN", "zh-CN"),
(CODEX_ADAPTER, "EN", "en-US"),
(CLAUDE_ADAPTER, "CN", "zh-CN"),
(CLAUDE_ADAPTER, "EN", "en-US"),
]
DIRECTIONS = [("CN", "zh-CN"), ("EN", "en-US")]


def _header_hash(adapter, direction: str) -> str:
"""Hash the rendered header template for a host adapter."""
source = adapter.source_root(ROOT, direction)
content = (source / HEADER_TEMPLATE_NAME).read_text()
content = content.replace("{{config_dir}}", adapter.config_dir or "")
return hashlib.sha256(f"{adapter.header_filename}\x00{content}".encode()).hexdigest()


def _tree_hash(direction: str) -> str:
"""Hash all skill files to detect content drift."""
skill_root = ROOT / "skills" / language_to_source_dir(direction) / "skills" / "sopify"
parts = [
f.relative_to(skill_root).as_posix().encode() + b"\x00" + f.read_bytes()
for f in sorted(skill_root.rglob("*"))
if f.is_file() and f.name not in IGNORE
]
return hashlib.sha256(b"\n".join(parts)).hexdigest()


def _payload_hash(direction: str) -> str:
"""Hash the fully rendered Copilot managed block payload."""
source = COPILOT_ADAPTER.source_root(ROOT, direction)
rendered = render_single_file(
source / HEADER_TEMPLATE_NAME, source / "skills" / "sopify", COPILOT_ADAPTER
)
return hashlib.sha256(rendered.encode()).hexdigest()


def main() -> int:
if not GOLDEN.exists():
return 0

snapshots: dict[str, str] = {}
for adapter, direction, locale in ADAPTERS:
snapshots[f"{adapter.host_name}:{locale}:header"] = _header_hash(adapter, direction)

for direction, locale in DIRECTIONS:
snapshots[f"copilot:{locale}:managed_block_payload"] = _payload_hash(direction)
snapshots[f"skills:{locale}:tree"] = _tree_hash(direction)

golden = json.loads(GOLDEN.read_text())
golden["snapshots"] = snapshots
GOLDEN.write_text(json.dumps(golden, indent=2, ensure_ascii=False) + "\n")
print(f" Golden snapshots regenerated ({len(snapshots)} entries).")
return 0


if __name__ == "__main__":
raise SystemExit(main())
11 changes: 10 additions & 1 deletion scripts/release-preflight.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,16 @@ PY
}

run_step "Check version consistency" bash "$ROOT_DIR/scripts/check-version-consistency.sh"
run_step "Check golden snapshots" python3 -m pytest "$ROOT_DIR/tests/test_golden_snapshots.py" -q

# Golden snapshots: auto-regenerate if stale, then verify
if ! python3 -m pytest "$ROOT_DIR/tests/test_golden_snapshots.py" -q 2>/dev/null; then
echo "[release-preflight] Golden snapshots stale — regenerating..."
python3 "$ROOT_DIR/scripts/regenerate-golden-snapshots.py"
git add "$ROOT_DIR/tests/golden-snapshots.json"
run_step "Re-check golden snapshots" python3 -m pytest "$ROOT_DIR/tests/test_golden_snapshots.py" -q
else
echo "[release-preflight] Check golden snapshots"
fi
run_step "Check builtin catalog drift" check_builtin_catalog_drift
run_step "Check context checkpoints" python3 "$ROOT_DIR/scripts/check-context-checkpoints.py" repo --root "$ROOT_DIR"
run_step "Run hard gate tests (contract + smoke + distribution)" python3 -m pytest "$ROOT_DIR/tests" -m "not implementation_mirror" -v
Expand Down
49 changes: 1 addition & 48 deletions scripts/release-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -298,53 +298,6 @@ replace_once "$SKILLS_EN" '^<!-- SOPIFY_VERSION: .* -->$' "<!-- SOPIFY_VERSION:
bash "$ROOT_DIR/scripts/check-version-consistency.sh"

# Regenerate golden snapshots after version bump (skips gracefully outside repo)
python3 -c "
import sys
sys.path.insert(0, '$ROOT_DIR')
try:
import hashlib, json
from pathlib import Path
from installer.hosts.base import HEADER_TEMPLATE_NAME, HostAdapter, render_single_file
from installer.hosts.claude import CLAUDE_ADAPTER
from installer.hosts.codex import CODEX_ADAPTER
from installer.hosts.copilot import COPILOT_ADAPTER
from installer.models import language_to_source_dir
except ImportError:
sys.exit(0)

R = Path('$ROOT_DIR')
G = R / 'tests/golden-snapshots.json'
if not G.exists():
sys.exit(0)
IGNORE = {'.DS_Store','Thumbs.db','__pycache__'}

def hh(a, d):
s = a.source_root(R, d)
c = (s/HEADER_TEMPLATE_NAME).read_text()
c = c.replace('{{config_dir}}', a.config_dir or '')
return hashlib.sha256(f'{a.header_filename}\x00{c}'.encode()).hexdigest()

def th(d):
sr = R/'skills'/language_to_source_dir(d)/'skills'/'sopify'
parts = [f.relative_to(sr).as_posix().encode()+b'\x00'+f.read_bytes()
for f in sorted(sr.rglob('*')) if f.is_file() and f.name not in IGNORE]
return hashlib.sha256(b'\n'.join(parts)).hexdigest()

def ph(d):
s = COPILOT_ADAPTER.source_root(R, d)
return hashlib.sha256(render_single_file(s/HEADER_TEMPLATE_NAME, s/'skills'/'sopify', COPILOT_ADAPTER).encode()).hexdigest()

snap = {}
for a,d,l in [(CODEX_ADAPTER,'CN','zh-CN'),(CODEX_ADAPTER,'EN','en-US'),(CLAUDE_ADAPTER,'CN','zh-CN'),(CLAUDE_ADAPTER,'EN','en-US')]:
snap[f'{a.host_name}:{l}:header'] = hh(a,d)
for d,l in [('CN','zh-CN'),('EN','en-US')]:
snap[f'copilot:{l}:managed_block_payload'] = ph(d)
snap[f'skills:{l}:tree'] = th(d)

g = json.loads(G.read_text())
g['snapshots'] = snap
G.write_text(json.dumps(g, indent=2, ensure_ascii=False)+'\n')
print(f' Golden snapshots regenerated ({len(snap)} entries).')
"
python3 "$ROOT_DIR/scripts/regenerate-golden-snapshots.py"

echo "Release sync completed successfully."
1 change: 1 addition & 0 deletions tests/test_release_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def _init_release_hook_fixture(root: Path, *, inject_sync_failure: bool = False)
"scripts/sync-skills.sh",
"scripts/check-version-consistency.sh",
"scripts/render-host-skills.py",
"scripts/regenerate-golden-snapshots.py",
".githooks/pre-commit",
".githooks/commit-msg",
):
Expand Down
Loading