changelog.py (7650B)
1 import os 2 from datetime import datetime, timezone 3 4 sha = os.environ.get('BUILD_SHA', 'unknown') 5 actor = os.environ.get('BUILD_ACTOR', 'unknown') 6 ref = os.environ.get('BUILD_REF', 'unknown') 7 timestamp = os.environ.get('BUILD_TIMESTAMP', '') 8 9 try: 10 dt = datetime.fromisoformat(timestamp).astimezone(timezone.utc) 11 build_time = dt.strftime('%B %d, %Y at %H:%M UTC') 12 except Exception: 13 build_time = timestamp 14 15 STATUS_LABELS = { 16 'A': 'Added', 17 'M': 'Modified', 18 'D': 'Removed', 19 'T': 'Type changed', 20 'U': 'Unmerged', 21 } 22 23 24 def status_label(code): 25 letter = code[0] 26 if letter == 'R': 27 return f'Renamed ({code[1:]}%)' if len(code) > 1 else 'Renamed' 28 if letter == 'C': 29 return f'Copied ({code[1:]}%)' if len(code) > 1 else 'Copied' 30 return STATUS_LABELS.get(letter, letter) 31 32 33 # ---- Pass 1: commit metadata + numstat (line/diff counts) ---- 34 commits = [] 35 commit_index = {} # full_sha -> index in commits 36 37 try: 38 with open('git_log_numstat.txt', 'r', encoding='utf-8') as f: 39 current = None 40 for raw_line in f: 41 line = raw_line.rstrip('\n') 42 if line.startswith('COMMIT|'): 43 parts = line.split('|') 44 if len(parts) == 5: 45 _, c_sha, c_msg, c_author, c_time = parts 46 try: 47 c_dt = datetime.fromisoformat(c_time).astimezone(timezone.utc) 48 c_time_fmt = c_dt.strftime('%b %d, %Y %H:%M UTC') 49 except Exception: 50 c_time_fmt = c_time 51 current = { 52 'sha': c_sha[:7], 53 'full_sha': c_sha, 54 'msg': c_msg, 55 'author': c_author, 56 'time': c_time_fmt, 57 'files': {}, # path -> {'add': int, 'del': int, 'status': str} 58 'order': [], # preserve file order 59 } 60 commit_index[c_sha] = len(commits) 61 commits.append(current) 62 continue 63 if not line.strip() or current is None: 64 continue 65 # numstat line: "<added>\t<deleted>\t<path>" (binary files use '-') 66 cols = line.split('\t') 67 if len(cols) == 3: 68 add, dele, path = cols 69 add_n = int(add) if add.isdigit() else 0 70 del_n = int(dele) if dele.isdigit() else 0 71 current['files'][path] = {'add': add_n, 'del': del_n, 'status': 'M'} 72 current['order'].append(path) 73 except FileNotFoundError: 74 pass 75 76 # ---- Pass 2: name-status (A/M/D/R/C) merged into the same commits ---- 77 try: 78 with open('git_log_namestatus.txt', 'r', encoding='utf-8') as f: 79 current = None 80 for raw_line in f: 81 line = raw_line.rstrip('\n') 82 if line.startswith('COMMIT|'): 83 parts = line.split('|') 84 if len(parts) == 2: 85 c_sha = parts[1] 86 idx = commit_index.get(c_sha) 87 current = commits[idx] if idx is not None else None 88 continue 89 if not line.strip() or current is None: 90 continue 91 cols = line.split('\t') 92 if len(cols) == 2: 93 code, path = cols 94 if path in current['files']: 95 current['files'][path]['status'] = code 96 else: 97 current['files'][path] = {'add': 0, 'del': 0, 'status': code} 98 current['order'].append(path) 99 elif len(cols) == 3: 100 code, old_path, new_path = cols 101 target = new_path 102 stats = current['files'].pop(old_path, {'add': 0, 'del': 0, 'status': code}) 103 stats['status'] = code 104 current['files'][target] = stats 105 if old_path in current['order']: 106 current['order'][current['order'].index(old_path)] = target 107 else: 108 current['order'].append(target) 109 except FileNotFoundError: 110 pass 111 112 ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline; margin-bottom:-2px; margin-right:6px;" class="lucide lucide-history"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg>' 113 114 115 def build_commit_block(c): 116 commit_url = f"https://github.com/notamitgamer/bsc/commit/{c['full_sha']}" 117 total_add = sum(v['add'] for v in c['files'].values()) 118 total_del = sum(v['del'] for v in c['files'].values()) 119 file_count = len(c['files']) 120 121 header = ( 122 f"### [`{c['sha']}`]({commit_url}) {c['msg']}\n\n" 123 f"**{c['author']}** committed on {c['time']}" 124 ) 125 126 if file_count: 127 stat_bits = [] 128 if total_add or total_del: 129 stat_bits.append(f"+{total_add} / -{total_del} lines") 130 stat_bits.append(f"{file_count} file{'s' if file_count != 1 else ''} changed") 131 header += f" · {' · '.join(stat_bits)}" 132 133 if not file_count: 134 return header + "\n" 135 136 file_lines = [] 137 for path in c['order']: 138 info = c['files'][path] 139 label = status_label(info['status']) 140 diff_bits = [] 141 if info['add']: 142 diff_bits.append(f"+{info['add']}") 143 if info['del']: 144 diff_bits.append(f"-{info['del']}") 145 diff_str = ' '.join(diff_bits) if diff_bits else 'binary/no diff' 146 file_lines.append(f"| `{path}` | {label} | {diff_str} |") 147 148 files_table = ( 149 "| File | Change | Lines |\n" 150 "| --- | --- | --- |\n" 151 + '\n'.join(file_lines) 152 ) 153 154 details = ( 155 "<details>\n" 156 f"<summary>Show {file_count} changed file{'s' if file_count != 1 else ''}</summary>\n\n" 157 f"{files_table}\n" 158 "</details>\n" 159 ) 160 161 return f"{header}\n\n{details}" 162 163 164 commit_blocks = [build_commit_block(c) for c in commits] 165 commit_section = '\n\n---\n\n'.join(commit_blocks) if commit_blocks else 'No commit history available.' 166 167 # VitePress docs/changelog.md 168 docs_content = f"""--- 169 title: '{ICON} Changelog' 170 description: 'Current build information and recent commit history.' 171 --- 172 173 # Build Info 174 175 ::: tip Important 176 Compare the Build ID (listed below) against the one in the [GitHub Changelog](https://github.com/notamitgamer/bsc/blob/main/CHANGELOG.md#latest-build) to verify that your browser is displaying the latest version. 177 ::: 178 179 - **Build ID** — <span style="word-break: break-all;">`{sha}`</span> 180 - **Triggered by** — [@{actor}](https://github.com/{actor}) 181 - **Branch** — `{ref}` 182 - **Build time** — {build_time} 183 184 ## Recent Commits 185 186 {commit_section} 187 """ 188 189 with open('docs/changelog.md', 'w', encoding='utf-8') as f: 190 f.write(docs_content) 191 192 # Root CHANGELOG.md for GitHub 193 root_content = f"""# Changelog 194 195 > Last build: {build_time} 196 197 ## Latest Build 198 199 - **Build ID** — `{sha}` 200 - **Triggered by** — [@{actor}](https://github.com/{actor}) 201 - **Branch** — `{ref}` 202 - **Build time** — {build_time} 203 204 ## Recent Commits 205 206 {commit_section} 207 208 --- 209 210 *This file is auto-generated on every deployment. For the live site, visit [code.amit.is-a.dev](https://code.amit.is-a.dev).* 211 *Check [/changelog](https://code.amit.is-a.dev/changelog) on the site to verify your browser is showing the latest build.* 212 """ 213 214 with open('CHANGELOG.md', 'w', encoding='utf-8') as f: 215 f.write(root_content) 216 217 print(f"Generated changelog with {len(commits)} commits.")