Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/artifact-cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# GitHub Action for Building and Uploading Babylon Features Artifact
name: Babylon Features Artifact CD

on:
push:
branches: [ "main" ]
# tags:
# - 'v*' # Trigger on version tags (e.g., v1.0.0)
workflow_dispatch:
inputs:
tag:
description: 'Release tag'
required: true
default: 'latest'

permissions:
contents: write

jobs:
build-and-upload:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox requests

- name: Run Artifact Upload Script
env:
BABYLON_API_GITHUB_PAT_TOKEN: ${{ secrets.BABYLON_PAT_CI }}
run: |
# The script will run tox -e build-artifact and upload to GitHub Releases
python artifact_upload.py --repo ${{ github.repository }} --tag ${{ github.ref_name }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,6 @@ __marimo__/

# Chroma local DB
chromadb/

# Any zipped artifacts
*.zip
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,32 @@ A local distribution of the package can be created either through
```
or
```shell
tos -e dist
tox -e dist
```
Since the build is dependent on `poetry`, the commands are equivalent.

### Artifact Deployment
This project can be built as a Zip module artifact for deployment. The artifact includes the `features/` package.

To build the artifact locally:
```shell
tox -e dist
```
This will create `babylon.zip` in the root directory, along with standard poetry distribution files in `dist/`.

### CI/CD
The project uses GitHub Actions for CI/CD.

- **babylon-features**: Runs on every push and pull request to `main`. It performs linting, formatting, and unit testing using `tox`.
- **Babylon Features Artifact CD**: Runs when a version tag (e.g., `v1.0.0`) is pushed or manually triggered. It builds the `babylon.zip` artifact using `tox -e dist` and uploads it to GitHub Releases using `artifact_upload.py`.

To manually trigger a deployment and upload to GitHub:
```shell
python artifact_upload.py --repo <owner>/<repo> --tag <tag>
```
Ensure `BABYLON_API_GITHUB_PAT_TOKEN` is set in your environment.


### Unit Tests
This project uses `pytest`. You can invoke tests in a poetry environment, via
```shell
Expand Down
147 changes: 147 additions & 0 deletions artifact_upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import argparse
import os
import subprocess
import sys
import requests

def run_tox_build():
"""
Executes tox to build the artifact using the dist environment.
"""
print("Building artifact with tox -e dist...")
try:
subprocess.run(["tox", "-e", "dist"], check=True)
print("Tox build successful.")
except subprocess.CalledProcessError as e:
print(f"Error: Tox build failed with exit code {e.returncode}")
sys.exit(1)
except FileNotFoundError:
print("Error: 'tox' command not found. Ensure tox is installed.")
sys.exit(1)

def get_release_by_tag(repo: str, tag: str, pat_token: str):
"""
Fetches a release by its tag.
"""
api_url = f"https://api.github.com/repos/{repo}/releases/tags/{tag}"
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {pat_token}"
}
response = requests.get(api_url, headers=headers)
if response.status_code == 200:
return response.json()
return None

def create_release(repo: str, tag: str, pat_token: str):
"""
Creates a new release on GitHub.
"""
print(f"Creating new release for tag: {tag}")
api_url = f"https://api.github.com/repos/{repo}/releases"
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {pat_token}"
}
data = {
"tag_name": tag,
"name": f"Babylon Artifact {tag}",
"body": "Automatically generated Babylon artifact.",
"draft": False,
"prerelease": False
}
response = requests.post(api_url, headers=headers, json=data)
response.raise_for_status()
return response.json()

def upload_artifact_to_release(repo: str, release_id: int, artifact_path: str, pat_token: str):
"""
Uploads a file as a release asset.
"""
artifact_name = os.path.basename(artifact_path)
print(f"Uploading {artifact_name} to release {release_id}...")

# First, check if asset already exists and delete it if so (to allow overwrite)
api_url = f"https://api.github.com/repos/{repo}/releases/{release_id}/assets"
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {pat_token}"
}
response = requests.get(api_url, headers=headers)
response.raise_for_status()
assets = response.json()
for asset in assets:
if asset['name'] == artifact_name:
print(f"Deleting existing asset: {artifact_name}")
delete_url = asset['url']
requests.delete(delete_url, headers=headers).raise_for_status()

# Upload the new asset
upload_url = f"https://uploads.github.com/repos/{repo}/releases/{release_id}/assets?name={artifact_name}"
headers["Content-Type"] = "application/zip"

with open(artifact_path, "rb") as f:
response = requests.post(upload_url, headers=headers, data=f)
response.raise_for_status()

print(f"Successfully uploaded {artifact_name} to GitHub Releases.")

def main():
"""
Main entry point for building and uploading Babylon artifacts to GitHub Releases.
The script:
1. Parses command-line arguments for repo, tag, and GitHub PAT token.
2. Determines the tag to use (defaults to 'latest').
3. Runs 'tox -e build-artifact' to generate 'babylon.zip' unless --skip-build is set.
4. Fetches or creates a GitHub Release for the specified tag.
5. Uploads 'babylon.zip' as a release asset (overwriting if it already exists).
"""
parser = argparse.ArgumentParser(description="Build and upload Babylon artifact to GitHub.")
parser.add_argument("--repo", required=True, help="GitHub repository (e.g., owner/repo)")
parser.add_argument("--tag", help="Release tag to upload to (defaults to 'latest' if not provided)")
parser.add_argument("--pat-token", default=os.environ.get("BABYLON_API_GITHUB_PAT_TOKEN"), help="GitHub PAT token")
parser.add_argument("--skip-build", action="store_true", help="Skip the tox build step")

args = parser.parse_args()

# Process tag information from GitHub Actions or manual input
# Use 'latest' if tag is not provided or if it's a branch name from GHA
tag = args.tag if args.tag and not args.tag.startswith("refs/heads/") else "latest"
if args.tag and "/" in args.tag and not args.tag.startswith("refs/tags/"):
# if it's a full ref but not a tag ref (e.g. refs/pull/...), default to latest
tag = "latest"
elif args.tag and args.tag.startswith("refs/tags/"):
# Clean up refs/tags/ prefix if present
tag = args.tag.replace("refs/tags/", "")

if not args.pat_token:
print("Error: GitHub PAT token not provided. Set BABYLON_API_GITHUB_PAT_TOKEN or use --pat-token.")
sys.exit(1)

artifact_path = "babylon.zip"

# Step 1: Build the artifact using tox as the source of truth
if not args.skip_build:
run_tox_build()

# Step 2: Ensure the artifact was actually built
if not os.path.exists(artifact_path):
print(f"Error: Artifact not found at {artifact_path}")
sys.exit(1)

try:
# Step 3: Find or create the GitHub Release
release = get_release_by_tag(args.repo, tag, args.pat_token)
if not release:
release = create_release(args.repo, tag, args.pat_token)

# Step 4: Upload the artifact as a release asset
upload_artifact_to_release(args.repo, release['id'], artifact_path, args.pat_token)
except requests.exceptions.RequestException as e:
print(f"GitHub API Error: {e}")
if e.response is not None:
print(f"Response: {e.response.text}")
sys.exit(1)

if __name__ == "__main__":
main()
File renamed without changes.
17 changes: 17 additions & 0 deletions docs/upload-artifact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
We are building a way to deploy `babylon_features` as an artifact, which is a Zip module.

## Task
Your task is to design and implement a Github CI workflow to build this python project with `tox`, and push the built artifact to Github.
- The workflow should be implemented in a Python script, named `artifact_upload.py`.
- There should be a CD flow in `.github` to deploy to Github artifacts.

## Credentials
The Guthub PAT should exist at `$BABYLON_API_GITHUB_PAT_TOKEN`.
If there are issues with credentials for pushing to Github, check with the user for instructions.

## TOX
Tox should be the source of truth for building the zip module.

## README and Documentation.
Update the README.md with any changes. Ensure the flow is well-documented and commented.

Empty file removed main.py
Empty file.
6 changes: 5 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,15 @@ allowlist_externals =

[testenv:dist]
description =
Build a distribution via poetry.
Build a distribution via poetry and a zip artifact.
deps =
-e .
commands =
poetry build
python -c "import shutil; shutil.make_archive('babylon', 'zip', '.', 'features')"
allowlist_externals =
poetry
python

;[testenv:download-spec]
;description = Download OpenAPI spec artifact
Expand Down