Skip to content

Commit 196a9b7

Browse files
JonZeollaclaude
andcommitted
refactor: extract zip template extraction to scripts/extract_template_zip.py
Move the inline Python zip extraction into a proper script file that can be tested and maintained independently. The CI downloads it from the raw GitHub URL since the repo cannot be checked out on Windows. Also add python3 --version check to Docker image verification. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 562be60 commit 196a9b7

2 files changed

Lines changed: 62 additions & 30 deletions

File tree

.github/workflows/ci.yml

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -129,37 +129,14 @@ jobs:
129129
git config --global user.email "ci@zenable.io"
130130
131131
# The template directory name contains NTFS-illegal characters
132-
# (double quotes, pipe). Use Python to extract the zip, renaming
133-
# the template dir to {{cookiecutter.project_name}} which is
134-
# NTFS-safe and renders identically for defaults (no spaces).
132+
# (double quotes, pipe). Use extract_template_zip.py to safely
133+
# extract and rename only the top-level template dir.
135134
zipUrl="https://github.com/${{ github.repository }}/archive/${TEMPLATE_REF}.zip"
135+
scriptUrl="https://raw.githubusercontent.com/${{ github.repository }}/${TEMPLATE_REF}/scripts/extract_template_zip.py"
136136
tmpdir=$(mktemp -d)
137137
curl -fsSL "$zipUrl" -o "$tmpdir/template.zip"
138-
139-
repoDir=$(python3 -c "
140-
import zipfile, os, sys
141-
zf, dest = zipfile.ZipFile(sys.argv[1]), sys.argv[2]
142-
for info in zf.infolist():
143-
parts = info.filename.split('/')
144-
# Only rename the top-level template dir (index 1) which has
145-
# NTFS-illegal chars (pipe, quotes). Nested cookiecutter dirs
146-
# like {{cookiecutter.project_slug}} are NTFS-safe and must be
147-
# preserved so cookiecutter renders them correctly.
148-
safe = []
149-
for i, p in enumerate(parts):
150-
if i == 1 and '{{cookiecutter.' in p:
151-
safe.append('{{cookiecutter.project_name}}')
152-
else:
153-
safe.append(p)
154-
target = os.path.join(dest, *[s for s in safe if s])
155-
if info.is_dir():
156-
os.makedirs(target, exist_ok=True)
157-
else:
158-
os.makedirs(os.path.dirname(target), exist_ok=True)
159-
with zf.open(info) as s, open(target, 'wb') as d:
160-
d.write(s.read())
161-
print(os.path.join(dest, os.listdir(dest)[0]))
162-
" "$tmpdir/template.zip" "$tmpdir/src")
138+
curl -fsSL "$scriptUrl" -o "$tmpdir/extract_template_zip.py"
139+
repoDir=$(python3 "$tmpdir/extract_template_zip.py" "$tmpdir/template.zip" "$tmpdir/src")
163140
164141
uvx --with gitpython cookiecutter "$repoDir" --no-input --output-dir "$RUNNER_TEMP"
165142
- name: Verify generated project
@@ -259,8 +236,10 @@ jobs:
259236
- name: Verify Docker image
260237
shell: bash
261238
run: |
262-
docker run --rm --entrypoint python3 \
263-
zenable-io/replace-me:latest \
239+
# Verify the Python runtime is functional
240+
docker run --rm --entrypoint python3 zenable-io/replace-me:latest --version
241+
# Verify the project module is importable and reports its version
242+
docker run --rm --entrypoint python3 zenable-io/replace-me:latest \
264243
-c "from replace_me import __version__; print(__version__)"
265244
- name: Verify zenable CLI
266245
shell: bash

scripts/extract_template_zip.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python3
2+
"""Extract a cookiecutter template from a GitHub archive zip.
3+
4+
The top-level template directory in the zip contains NTFS-illegal characters
5+
(pipe, double-quotes) from the Jinja2 expression in
6+
``{{cookiecutter.project_name|replace(" ", "")}}``. This script renames
7+
**only** that top-level directory to the safe ``{{cookiecutter.project_name}}``
8+
form while preserving nested cookiecutter directories like
9+
``{{cookiecutter.project_slug}}`` so cookiecutter renders them correctly.
10+
11+
Usage:
12+
python extract_template_zip.py <zip_path> <dest_dir>
13+
14+
Prints the path to the extracted template root on stdout.
15+
"""
16+
17+
import os
18+
import sys
19+
import zipfile
20+
21+
22+
def extract(zip_path: str, dest: str) -> str:
23+
"""Extract the zip, renaming only the top-level template directory."""
24+
with zipfile.ZipFile(zip_path) as zf:
25+
for info in zf.infolist():
26+
parts = info.filename.split("/")
27+
28+
safe = []
29+
for i, part in enumerate(parts):
30+
# Index 0 is the repo root (e.g. ai-native-python-<sha>).
31+
# Index 1 is the template directory with NTFS-illegal chars.
32+
if i == 1 and "{{cookiecutter." in part:
33+
safe.append("{{cookiecutter.project_name}}")
34+
else:
35+
safe.append(part)
36+
37+
target = os.path.join(dest, *[s for s in safe if s])
38+
39+
if info.is_dir():
40+
os.makedirs(target, exist_ok=True)
41+
else:
42+
os.makedirs(os.path.dirname(target), exist_ok=True)
43+
with zf.open(info) as src, open(target, "wb") as dst:
44+
dst.write(src.read())
45+
46+
return os.path.join(dest, os.listdir(dest)[0])
47+
48+
49+
if __name__ == "__main__":
50+
if len(sys.argv) != 3:
51+
print(f"Usage: {sys.argv[0]} <zip_path> <dest_dir>", file=sys.stderr)
52+
sys.exit(1)
53+
print(extract(sys.argv[1], sys.argv[2]))

0 commit comments

Comments
 (0)