Upload Assets to GitHub Release with Python Mock
Simulates uploading binary and text assets to a GitHub release using a mock server, returning structured metadata for each upload.
Python code
53 linesimport json
import os
import tempfile
from datetime import datetime
class ReleaseUploader:
"""Simulates uploading assets to a release with a mock server."""
def __init__(self, owner: str, repo: str, tag: str):
self.owner = owner
self.repo = repo
self.tag = tag
self.uploaded = {}
def mock_upload(self, file_path: str, asset_name: str = None) -> dict:
"""Uploads a file to the mock release and returns the response."""
if not os.path.isfile(file_path):
raise FileNotFoundError(f"No such file: {file_path}")
upload_id = f"asset_{len(self.uploaded) + 1:03d}"
asset_name = asset_name or os.path.basename(file_path)
size = os.path.getsize(file_path)
response = {
"url": f"https://mock.example.com/repos/{self.owner}/{self.repo}/releases/{self.tag}/assets/{asset_name}",
"id": upload_id,
"name": asset_name,
"size": size,
"uploaded_at": datetime.utcnow().isoformat(),
"state": "uploaded"
}
self.uploaded[upload_id] = response
return response
if __name__ == "__main__":
uploader = ReleaseUploader("octocat", "hello-world", "v1.0.0")
# Simulate uploading two files
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f1, \
tempfile.NamedTemporaryFile(mode='w', suffix='.zip', delete=False) as f2:
f1.write("Release notes content")
f2.write("binary content placeholder")
file1, file2 = f1.name, f2.name
upload1 = uploader.mock_upload(file1, "README.txt")
upload2 = uploader.mock_upload(file2)
print(json.dumps(list(uploader.uploaded.values()), indent=2))
# Cleanup temp files
os.unlink(file1)
os.unlink(file2)
Output
[
{
"url": "https://mock.example.com/repos/octocat/hello-world/releases/v1.0.0/assets/README.txt",
"id": "asset_001",
"name": "README.txt",
"size": 20,
"uploaded_at": "2024-01-01T12:00:00.000000",
"state": "uploaded"
},
{
"url": "https://mock.example.com/repos/octocat/hello-world/releases/v1.0.0/assets/tmpabc123.zip",
"id": "asset_002",
"name": "tmpabc123.zip",
"size": 25,
"uploaded_at": "2024-01-01T12:00:00.000001",
"state": "uploaded"
}
]
How it works
The ReleaseUploader class wraps upload logic in a reusable object, storing results in a dictionary keyed by generated upload IDs. mock_upload checks file existence, creates a deterministic URL structure, and records file size via os.path.getsize. Using datetime.utcnow().isoformat() gives a timestamp that mirrors GitHub's API response format. The if __name__ == "__main__" guard keeps the demo isolated from import. This pattern lets you swap the mock body for a real requests.post call later.
Common mistakes
- Forgetting to close or clean up temp files, leaking disk space
- Assuming asset_name is optional when you want a stable filename
- Not handling FileNotFoundError before building the response
- Using local time instead of UTC for timestamps that match API conventions
Variations
- Swap mock_upload for real GitHub API calls with `requests.post` and `Authorization: Bearer` headers
- Use `pathlib.Path` to manage file paths and size checks instead of `os` module
Real-world use cases
- Testing CI/CD pipelines locally that publish build artifacts to GitHub releases.
- Building a staging script that collects multiple binaries and archives for a versioned release.
- Writing integration tests against a fake release endpoint so uploads don't hit the live API.
Sponsored
More from Git + Python
- Amend Last Commit Message in Python easy
- Bisect Good Bad Automation Script in Python easy
- Build a Simple Log Graph in Python easy
- Bump Semantic Version Git Tag in Python easy
- Count Unique Contributors from Git Shortlog in Python easy
- Create a Mock GitHub Release API in Python for Testing gh CLI easy
Keep learning
Related tutorials and quizzes for this topic.