Skip to content

Commit 03502d7

Browse files
authored
Automated workflow for updating changelog images (#2405)
* Add the .py and requirements.txt for hashing changelog images * Update the python script to have the correct output formats * Add a github workflow for all of this
1 parent f79dd35 commit 03502d7

File tree

3 files changed

+231
-0
lines changed

3 files changed

+231
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
name: Update Changelog Assets
2+
3+
on:
4+
push:
5+
paths:
6+
- 'CHANGELOG.md'
7+
8+
jobs:
9+
update-assets:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
# 1. Checkout the repository
14+
- name: Checkout Repository
15+
uses: actions/checkout@v3
16+
with:
17+
# Disable automatic token usage to allow `create-pull-request` to handle authentication
18+
persist-credentials: false
19+
fetch-depth: 0 # Fetch all history for accurate diff detection
20+
21+
# 2. Set up Python environment
22+
- name: Set Up Python
23+
uses: actions/setup-python@v4
24+
with:
25+
python-version: '3.x' # Specify the Python version you need
26+
27+
# 3. Install dependencies
28+
- name: Install Dependencies
29+
run: |
30+
python -m pip install --upgrade pip
31+
pip install -r scripts/python/requirements.txt
32+
33+
# 4. Run the Python script to process images and update CHANGELOG.md
34+
- name: Run Changelog Assets Script
35+
run: |
36+
python scripts/python/process_changelog_images.py
37+
38+
# 5. Configure Git for committing changes
39+
- name: Configure Git
40+
run: |
41+
git config --global user.name 'github-actions[bot]'
42+
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
43+
44+
# 6. Check for changes
45+
- name: Check for Changes
46+
id: changes
47+
run: |
48+
# Add all changes to staging
49+
git add .
50+
51+
# Check if there are any staged changes
52+
if git diff --cached --quiet; then
53+
echo "no_changes=true" >> $GITHUB_OUTPUT
54+
else
55+
echo "no_changes=false" >> $GITHUB_OUTPUT
56+
fi
57+
58+
# 7. Create Pull Request if changes exist
59+
- name: Create Pull Request
60+
if: steps.changes.outputs.no_changes == 'false'
61+
uses: peter-evans/create-pull-request@v5
62+
with:
63+
# Use the default GITHUB_TOKEN provided by GitHub Actions
64+
token: ${{ secrets.GITHUB_TOKEN }}
65+
66+
# Commit message for the changes
67+
commit-message: Update changelog assets and links
68+
69+
# Branch name for the PR (includes run ID for uniqueness)
70+
branch: update-changelog-assets-${{ github.run_id }}
71+
72+
# Title of the Pull Request
73+
title: "Update Changelog Assets"
74+
75+
# Body of the Pull Request
76+
body: |
77+
This PR updates the changelog assets by downloading and converting images to `.webp` format and updating the links in `CHANGELOG.md`.
78+
79+
# Labels to apply to the Pull Request (optional)
80+
labels: automated-update
81+
82+
# Target branch for the PR (default is the repository's default branch)
83+
# You can specify a different base branch if needed
84+
# base: main
85+
86+
# Automatically delete the branch after the PR is merged (optional)
87+
delete-branch: true
88+
89+
# If a PR already exists for the same changes, the action will add new commits to it
90+
# To prevent duplicate PRs, ensure unique branch names or handle accordingly
+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import os
2+
import re
3+
import requests
4+
import hashlib
5+
from PIL import Image
6+
from io import BytesIO
7+
8+
# Updates all the links in the CHANGELOG.md file that point to PNG images to
9+
# WebP format hashed and stored locally in the 'docs/changelog-assets' directory.
10+
11+
# Configuration
12+
CHANGELOG_PATH = 'CHANGELOG.md'
13+
ASSETS_DIR = os.path.join('docs', 'changelog-assets')
14+
IMAGE_URL_PATTERN = re.compile(r'\[([^\]]+)\]\((https://[^)]+\.png)\)')
15+
16+
def ensure_assets_dir():
17+
"""Ensure that the assets directory exists."""
18+
os.makedirs(ASSETS_DIR, exist_ok=True)
19+
print(f"Assets directory ensured at: {ASSETS_DIR}")
20+
21+
def read_changelog():
22+
"""Read the content of the CHANGELOG.md file."""
23+
with open(CHANGELOG_PATH, 'r', encoding='utf-8') as file:
24+
content = file.read()
25+
print(f"Read {len(content)} characters from {CHANGELOG_PATH}")
26+
return content
27+
28+
def write_changelog(content):
29+
"""Write the updated content back to the CHANGELOG.md file."""
30+
with open(CHANGELOG_PATH, 'w', encoding='utf-8') as file:
31+
file.write(content)
32+
print(f"Updated {CHANGELOG_PATH}")
33+
34+
def find_image_links(content):
35+
"""Find all markdown links to .png images."""
36+
matches = IMAGE_URL_PATTERN.findall(content)
37+
unique_urls = list(set(url for _, url in matches))
38+
print(f"Found {len(unique_urls)} unique image URLs to process.")
39+
return unique_urls
40+
41+
def download_image(url):
42+
"""Download image from the given URL."""
43+
try:
44+
response = requests.get(url, timeout=10)
45+
response.raise_for_status()
46+
print(f"Downloaded image from {url}")
47+
return response.content
48+
except requests.RequestException as e:
49+
print(f"Error downloading {url}: {e}")
50+
return None
51+
52+
def convert_to_webp(image_data):
53+
"""Convert image data to WebP format."""
54+
try:
55+
with Image.open(BytesIO(image_data)) as img:
56+
with BytesIO() as output:
57+
img.save(output, format='WEBP', quality=80)
58+
webp_data = output.getvalue()
59+
print("Converted image to WebP format.")
60+
return webp_data
61+
except Exception as e:
62+
print(f"Error converting image to WebP: {e}")
63+
return None
64+
65+
def hash_webp(webp_data):
66+
"""
67+
Hash the WebP data using BLAKE2b with a digest size of 128 bits (16 bytes)
68+
to match `b2sum --length=128`.
69+
"""
70+
blake2b_hash = hashlib.blake2b(webp_data, digest_size=16).hexdigest()
71+
print(f"Hashed WebP data to {blake2b_hash}")
72+
return blake2b_hash
73+
74+
def save_webp(webp_data, hash_digest):
75+
"""Save the WebP data to the assets directory with the hash as filename."""
76+
filename = f"{hash_digest}.webp"
77+
filepath = os.path.join(ASSETS_DIR, filename)
78+
if not os.path.exists(filepath):
79+
with open(filepath, 'wb') as file:
80+
file.write(webp_data)
81+
print(f"Saved WebP image to {filepath}")
82+
else:
83+
print(f"WebP image already exists at {filepath}")
84+
return filepath
85+
86+
def process_images(urls):
87+
"""Process all image URLs: download, convert, hash, and save."""
88+
url_to_new_path = {}
89+
for url in urls:
90+
print(f"Processing URL: {url}")
91+
image_data = download_image(url)
92+
if not image_data:
93+
continue
94+
95+
webp_data = convert_to_webp(image_data)
96+
if not webp_data:
97+
continue
98+
99+
hash_digest = hash_webp(webp_data)
100+
saved_path = save_webp(webp_data, hash_digest)
101+
102+
# Store the relative path for replacement
103+
relative_path = os.path.relpath(saved_path, start=os.path.dirname(CHANGELOG_PATH))
104+
relative_path = relative_path.replace(os.sep, '/')
105+
106+
# Ensure the path starts with './'
107+
if not relative_path.startswith(('.', '/')):
108+
relative_path = f'./{relative_path}'
109+
110+
url_to_new_path[url] = relative_path
111+
print(f"Updated path for {url}: {relative_path}")
112+
return url_to_new_path
113+
114+
def update_changelog(content, url_mapping):
115+
"""Update the changelog content with new relative WebP paths."""
116+
def replace_url(match):
117+
text, url = match.groups()
118+
new_url = url_mapping.get(url, url)
119+
return f'[{text}]({new_url})'
120+
121+
updated_content = IMAGE_URL_PATTERN.sub(replace_url, content)
122+
print("Changelog content updated with new image paths.")
123+
return updated_content
124+
125+
def main():
126+
ensure_assets_dir()
127+
content = read_changelog()
128+
image_urls = find_image_links(content)
129+
url_mapping = process_images(image_urls)
130+
if not url_mapping:
131+
print("No images were processed. Exiting.")
132+
return
133+
updated_content = update_changelog(content, url_mapping)
134+
write_changelog(updated_content)
135+
print("All done!")
136+
137+
if __name__ == "__main__":
138+
main()
139+

scripts/python/requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
requests>=2.25.1
2+
Pillow>=9.0.0

0 commit comments

Comments
 (0)