forked from Konstantin84UKR/Tetris_WebGPU
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeploy.py
More file actions
247 lines (209 loc) · 8.68 KB
/
Copy pathdeploy.py
File metadata and controls
247 lines (209 loc) · 8.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#!/usr/bin/env python3
"""
project_deploy_template.py
Copy this file into your project as `deploy.py` (or deploy_contabo.py).
Customize the constants at the top for your project.
Usage:
1. Build your project: npm run build:all (or python deploy.py --build)
2. python deploy.py
This script contacts https://storage.noahcohn.com (your Contabo storage manager)
to upload your entire build as a single zip archive. The server extracts it and
pushes all files over one persistent SFTP connection — much faster than uploading
files individually.
Actual FTP/SFTP credentials never leave the VPS.
Requirements:
pip install requests
"""
import argparse
import hashlib
import io
import os
import subprocess
import sys
import zipfile
from pathlib import Path
from typing import Optional
import requests
# ============================================================
# PER-PROJECT CONFIGURATION - EDIT THESE
# ============================================================
PROJECT_NAME: str = 'tetris-webgpu'
BUILD_DIR: str = 'dist'
CONTABO_BASE_URL: str = "https://storage.noahcohn.com"
DEPLOY_FOLDER: str = "" # override remote target folder; empty = use PROJECT_NAME
# Deploy token — read from the environment only; never hardcode secrets here.
# Set via environment: export DEPLOY_TOKEN="your_long_token_from_vps_env"
DEPLOY_TOKEN: Optional[str] = os.environ.get("DEPLOY_TOKEN")
# ============================================================
# Assets that must be present in dist/ for glass/gold block sampling to work.
REQUIRED_ASSETS = (
'index.html',
'block.png',
'release.wasm',
)
def md5_file(path: Path) -> str:
digest = hashlib.md5()
with path.open('rb') as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b''):
digest.update(chunk)
return digest.hexdigest()
def run_build() -> bool:
print('Running npm run build:all ...')
try:
subprocess.run(['npm', 'run', 'build:all'], check=True)
return True
except FileNotFoundError:
print('ERROR: npm not found on PATH.')
return False
except subprocess.CalledProcessError as exc:
print(f'ERROR: build failed with exit code {exc.returncode}')
return False
def preflight(build_path: Path) -> bool:
ok = True
print('Preflight checks:')
for rel in REQUIRED_ASSETS:
asset = build_path / rel
if not asset.is_file():
print(f" ✗ Missing required asset: {rel}")
ok = False
continue
if rel == 'block.png':
digest = md5_file(asset)
print(f" ✓ {rel} ({asset.stat().st_size / 1024:.1f} KB, md5={digest[:8]}…)")
else:
print(f" ✓ {rel} ({asset.stat().st_size / 1024:.1f} KB)")
if not DEPLOY_TOKEN:
print(' ✗ DEPLOY_TOKEN is not set — upload will be rejected (403).')
ok = False
else:
print(' ✓ DEPLOY_TOKEN is set')
print()
return ok
def fetch_remote_sizes(target_folder, target_site="test"):
"""Ask the VPS for {rel_path: bytes} already on the deploy target."""
base = CONTABO_BASE_URL.rstrip("/")
url = f"{base}/api/deploy/{PROJECT_NAME}/sizes"
headers = {}
token = globals().get("DEPLOY_TOKEN")
if token:
headers["X-Deploy-Token"] = token
params = {"target_site": target_site or "test"}
if target_folder:
params["target_folder"] = target_folder
try:
response = requests.get(url, params=params, headers=headers, timeout=60)
if response.status_code == 200:
files = response.json().get("files") or {}
print(f"Remote size map: {len(files)} file(s)")
return {str(k).replace("\\", "/"): int(v) for k, v in files.items()}
print(f" ! sizes HTTP {response.status_code}; uploading all files")
except Exception as exc:
print(f" ! Could not fetch remote sizes ({exc}); uploading all files")
return {}
def build_zip(build_path: Path, skip_sizes=None) -> bytes:
"""Zip the contents of build_path into an in-memory archive."""
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for file in sorted(build_path.rglob("*")):
if file.is_dir():
continue
rel = file.relative_to(build_path)
# Skip common junk
parts = rel.parts
if any(p in (".git", "node_modules", "__pycache__") for p in parts):
continue
rel_s = str(rel).replace("\\", "/")
local_size = file.stat().st_size
if (skip_sizes or {}).get(rel_s) == local_size:
print(f" = {rel} ({local_size} bytes, unchanged)")
continue
zf.write(file, rel_s)
print(f" + {rel}")
return buf.getvalue()
def deploy_bundle(build_path: Path) -> bool:
"""Zip the build and upload it as a single bundle."""
target_folder = DEPLOY_FOLDER or PROJECT_NAME
url = f"{CONTABO_BASE_URL}/api/deploy/{PROJECT_NAME}/bundle"
headers = {}
if DEPLOY_TOKEN:
headers["X-Deploy-Token"] = DEPLOY_TOKEN
print("Building zip archive...")
target_folder_for_sizes = globals().get("DEPLOY_FOLDER") or globals().get("TARGET_FOLDER") or PROJECT_NAME
if "target_folder" in locals() and target_folder:
target_folder_for_sizes = target_folder
target_site_for_sizes = globals().get("DEPLOY_TARGET", "test")
print("Checking remote file sizes...")
skip_sizes = fetch_remote_sizes(target_folder_for_sizes, target_site_for_sizes)
zip_bytes = build_zip(build_path, skip_sizes)
print(f"Archive size: {len(zip_bytes) / 1024:.1f} KB\n")
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as _zf:
if not _zf.namelist():
print("All files identical in size on the target; nothing to upload.")
return True
print("Uploading bundle...")
try:
response = requests.post(
url,
files={"bundle": ("build.zip", zip_bytes, "application/zip")},
data={"target_folder": target_folder},
headers=headers,
timeout=300,
)
except Exception as exc:
print(f" \u2717 Upload exception: {exc}")
return False
if response.status_code == 200:
data = response.json()
print(f" \u2713 {data.get('uploaded', 0)} files uploaded")
if data.get("failed"):
print(" Failures:")
for f in data["failed"]:
print(f" \u2717 {f['path']}: {f['error']}")
return not data.get("failed")
else:
print(f" \u2717 {response.status_code}: {response.text[:400]}")
return False
def main():
parser = argparse.ArgumentParser(description='Deploy Tetris WebGPU dist/ to Contabo storage.')
parser.add_argument(
'--build',
action='store_true',
help='Run npm run build:all before deploying (recommended after shader or block.png edits).',
)
args = parser.parse_args()
print(f"\n=== Deploying '{PROJECT_NAME}' via Contabo -> storage.1ink.us ===\n")
print('Note: GitHub Pages (konstantin84ukr.github.io) is a separate host — this script')
print('only updates the Contabo mirror (e.g. test.1ink.us/tetris-webgpu/).\n')
if args.build:
if not run_build():
sys.exit(1)
print()
build_path = Path(BUILD_DIR)
if not build_path.exists() or not build_path.is_dir():
print(f"ERROR: Build directory '{BUILD_DIR}/' does not exist.")
print("Run `npm run build:all` first, or re-run with `--build`.")
sys.exit(1)
if not preflight(build_path):
if not DEPLOY_TOKEN:
print('ERROR: DEPLOY_TOKEN is required for Contabo deploy uploads.')
print('Set it in your shell or Cloud Agent environment secrets, then retry:')
print(' export DEPLOY_TOKEN="<token-from-vps-env>"')
print(' python deploy.py')
else:
print('ERROR: Preflight failed — fix the build before deploying.')
sys.exit(1)
try:
health = requests.get(f"{CONTABO_BASE_URL}/api/deploy/health", timeout=10)
if health.status_code == 200:
print(f"Contabo deploy service: {health.json().get('status', 'unknown')}")
except Exception:
print("Warning: Could not contact storage.noahcohn.com (continuing anyway).")
print()
success = deploy_bundle(build_path)
print(f"\n=== {'Deployment complete' if success else 'Deployment finished with errors'} ===")
if success:
print('After deploy: hard-refresh the browser (Ctrl+Shift+R) — block.png is cache-busted')
print('via ?v=<md5> in the built bundle, but HTML/JS may still be cached briefly.')
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()