Pipeline Release: Use Add-On Bundle to Update Add-Ons #269
@ -2,7 +2,7 @@ Pipeline release is a script to package all addons into a single zip on the pipe
|
||||
|
||||
## Prerequisite
|
||||
In order to use this tool you need:
|
||||
- GIT
|
||||
- GIT & GIT LFS installed
|
||||
- Python 3.11+
|
||||
- [Requests Module](https://requests.readthedocs.io/en/latest/)
|
||||
|
||||
|
730
scripts/pipeline-release/pipeline_release.py
Normal file → Executable file
730
scripts/pipeline-release/pipeline_release.py
Normal file → Executable file
@ -1,582 +1,252 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
#
|
||||
# (c) 2021, Blender Foundation
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import zipfile
|
||||
import hashlib
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
import shutil
|
||||
import argparse
|
||||
import re
|
||||
from typing import Pattern
|
||||
import datetime
|
||||
import hashlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
import sys
|
||||
import requests
|
||||
import json
|
||||
from requests import Response
|
||||
|
||||
REPO_ROOT_DIR = Path(__file__).parent.parent.parent
|
||||
# BORROWED FROM https://github.com/pawamoy/git-changelog/blob/master/src/git_changelog/commit.py
|
||||
TYPES: dict[str, str] = {
|
||||
"add": "Added",
|
||||
"fix": "Fixed",
|
||||
"change": "Changed",
|
||||
"remove": "Removed",
|
||||
"merge": "Merged",
|
||||
"doc": "Documented",
|
||||
"breaking": "Breaking",
|
||||
}
|
||||
BASE_PATH = "https://projects.blender.org/api/v1"
|
||||
REPO_PATH = '/studio/blender-studio-pipeline'
|
||||
RELEASE_PATH = BASE_PATH + f'/repos{REPO_PATH}/releases'
|
||||
TAG_PATH = BASE_PATH + f'/repos{REPO_PATH}/tags'
|
||||
API_TOKEN = None
|
||||
|
||||
# GITEA LOGIN SETTINGS
|
||||
api_token_file = Path(__file__).parent.joinpath("api_token.env")
|
||||
if not api_token_file.exists():
|
||||
print("API Token File not Found")
|
||||
api_token = open(api_token_file, 'r').read()
|
||||
base_url = 'https://projects.blender.org'
|
||||
api_path = f"{base_url}/api/v1"
|
||||
repo_path = '/studio/blender-studio-pipeline'
|
||||
release_path = f'/repos{repo_path}/releases'
|
||||
tag_path = f'/repos{repo_path}/tags'
|
||||
RELEASE_TITLE = "Blender Studio Add-Ons Latest"
|
||||
RELEASE_VERSION = "latest"
|
||||
RELEASE_DESCRIPTION = "Latest Release of Blender Studio Pipeline Add-Ons"
|
||||
|
||||
ZIP_NAME = "blender_studio_add-ons_latest"
|
||||
|
||||
|
||||
def parse_commit(commit_message: str) -> dict[str, str]:
|
||||
"""
|
||||
Parse the type of the commit given its subject.
|
||||
Arguments:
|
||||
commit_subject: The commit message subject.
|
||||
def main():
|
||||
get_api_token()
|
||||
latest_release = get_release()
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix=ZIP_NAME + "_"))
|
||||
release_files = create_latest_addons_zip(ZIP_NAME, temp_dir)
|
||||
remove_existing_release_assets(latest_release["id"])
|
||||
for file in release_files:
|
||||
upload_asset_to_release(latest_release["id"], file)
|
||||
shutil.rmtree(temp_dir)
|
||||
print("Blender Studio Add-Ons Successfully Released")
|
||||
|
||||
|
||||
def remove_existing_release_assets(release_id: int) -> None:
|
||||
"""Removes all existing release assets for the given release ID.
|
||||
|
||||
Args:
|
||||
release_id (int): The ID of the release to remove assets from.
|
||||
|
||||
Returns:
|
||||
Dict containing commit message and type
|
||||
None
|
||||
"""
|
||||
type = ""
|
||||
# Split at first colon to remove prefix from commit
|
||||
if ": " in commit_message:
|
||||
message_body = commit_message.split(': ')[1]
|
||||
else:
|
||||
message_body = commit_message
|
||||
type_regex: Pattern = re.compile(r"^(?P<type>(%s))" % "|".join(TYPES.keys()), re.I)
|
||||
breaking_regex: Pattern = re.compile(
|
||||
r"^break(s|ing changes?)?[ :].+$",
|
||||
re.I | re.MULTILINE,
|
||||
|
||||
all_assets = send_get_request(RELEASE_PATH + f"/{release_id}/assets").json()
|
||||
for asset in all_assets:
|
||||
if asset["name"] == ZIP_NAME + ".zip" or asset["name"] == ZIP_NAME + ".zip.sha256":
|
||||
send_delete_request(RELEASE_PATH + f"/{release_id}/assets/{asset['id']}")
|
||||
print(f"Deleted {asset['name']} created on: {asset['created_at']}")
|
||||
|
||||
|
||||
def upload_asset_to_release(release_id: int, file: str) -> None:
|
||||
"""Uploads an asset to the specified release.
|
||||
|
||||
Args:
|
||||
release_id (int): The id of the release to upload to.
|
||||
file (str): The path to the file to upload.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
file_name = Path(file.name).name
|
||||
payload = open(file, 'rb')
|
||||
file_content = [
|
||||
('attachment', (file_name, payload, 'application/zip')),
|
||||
]
|
||||
print(f"Uploading '{file_name}'......", end="")
|
||||
response = requests.post(
|
||||
url=f"{RELEASE_PATH}/{release_id}/assets?name={file_name}&token={API_TOKEN}",
|
||||
files=file_content,
|
||||
)
|
||||
|
||||
type_match = type_regex.match(message_body)
|
||||
if type_match:
|
||||
type = TYPES.get(type_match.groupdict()["type"].lower(), "")
|
||||
if bool(breaking_regex.search(message_body)):
|
||||
type = "Breaking"
|
||||
return {
|
||||
"message": message_body,
|
||||
"type": type,
|
||||
response.raise_for_status()
|
||||
|
||||
if not response.status_code == 201:
|
||||
print(f"Failed to upload.")
|
||||
else:
|
||||
print(f"Completed")
|
||||
|
||||
|
||||
def get_release() -> dict:
|
||||
"""Gets the latest release matching the configured title and version.
|
||||
|
||||
Removes any existing release with the same title and version first before
|
||||
returning the latest release to ensure it represents the current commit.
|
||||
|
||||
Returns:
|
||||
dict: The release object for the latest matching release.
|
||||
"""
|
||||
|
||||
# Remove Previous Release so Release is always based on Current Commit
|
||||
for release in send_get_request(RELEASE_PATH).json():
|
||||
if release["name"] == RELEASE_TITLE and release["tag_name"] == RELEASE_VERSION:
|
||||
send_delete_request(RELEASE_PATH + f"/{release['id']}")
|
||||
send_delete_request(TAG_PATH + f"/{release['tag_name']}")
|
||||
return create_new_release()
|
||||
|
||||
|
||||
def create_new_release() -> dict:
|
||||
"""Create a new release on Gitea with the given title, version and description.
|
||||
|
||||
Makes a POST request to the Gitea API to create a new release with the specified
|
||||
parameters. Checks if a tag with the same version already exists first. If not,
|
||||
creates the tag before creating the release.
|
||||
|
||||
Returns:
|
||||
dict: The release object for the latest matching release.
|
||||
|
||||
"""
|
||||
# Create New Tag
|
||||
existing_tag = send_get_request(TAG_PATH + f'/{RELEASE_VERSION}')
|
||||
if existing_tag.status_code == 404:
|
||||
tag_content = {
|
||||
"message": RELEASE_DESCRIPTION,
|
||||
"tag_name": RELEASE_VERSION,
|
||||
"target": f"main",
|
||||
}
|
||||
|
||||
send_post_request(TAG_PATH, tag_content)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--commit",
|
||||
help="Find commit with this message and use it as the last version.",
|
||||
type=str,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--name",
|
||||
help="Only update the addon corrisponding to this name(s).",
|
||||
type=str,
|
||||
)
|
||||
# Create New Release
|
||||
release_content = {
|
||||
"body": RELEASE_DESCRIPTION,
|
||||
"draft": False,
|
||||
"name": RELEASE_TITLE,
|
||||
"prerelease": False,
|
||||
"tag_name": RELEASE_VERSION,
|
||||
"target_commitish": "string", # will default to latest
|
||||
}
|
||||
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
help="Provide a string for the output path of generated zips",
|
||||
type=str,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--major",
|
||||
help="Bump the major version number, otherwise bump minor version number",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--test",
|
||||
help="Test release system by only running locally and skip committing/uploading to release",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--reuse_lastest_release",
|
||||
help="Add new packages to the lastest avaliable release",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
help="Bump version even if no commits are found",
|
||||
action="store_true",
|
||||
)
|
||||
return send_post_request(RELEASE_PATH, release_content).json()
|
||||
|
||||
|
||||
def cli_command(command: str) -> subprocess:
|
||||
"""Run command in CLI and capture it's output
|
||||
Arguments:
|
||||
command: String of command to run in CLI.
|
||||
def get_api_token() -> None:
|
||||
"""Get API token from environment file.
|
||||
|
||||
Reads the API token from the api_token.env file and assigns it to the global
|
||||
API_TOKEN variable. Exits with error if file not found. Exists if API token is invalid.
|
||||
|
||||
"""
|
||||
output = subprocess.run(command.split(' '), capture_output=True, encoding="utf-8")
|
||||
return output
|
||||
global API_TOKEN
|
||||
api_token_file = Path(__file__).parent.joinpath("api_token.env")
|
||||
if not api_token_file.exists():
|
||||
print("API Token File not Found")
|
||||
sys.exit(1)
|
||||
API_TOKEN = open(api_token_file, 'r').read()
|
||||
# Don't use send_get_request() so we can print custom error message to user
|
||||
response = requests.get(url=f"{BASE_PATH}/settings/api?token={API_TOKEN}")
|
||||
if response.status_code != 200:
|
||||
print("API Token is invalid")
|
||||
print(f"Error: {response.status_code}: '{response.reason}'")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def exit_program(message: str):
|
||||
print(message)
|
||||
sys.exit(0)
|
||||
def create_latest_addons_zip(name: str, temp_dir: Path):
|
||||
"""Generate a pipeline release.
|
||||
|
||||
Args:
|
||||
name (str): The name of the release.
|
||||
|
||||
Returns:
|
||||
list: A list containing the path to the zipped release and checksum file.
|
||||
"""
|
||||
|
||||
output_dir = Path(temp_dir).joinpath(name)
|
||||
output_dir.mkdir()
|
||||
addons_dir = Path(__file__).parents[2].joinpath("scripts-blender/addons")
|
||||
|
||||
zipped_release = shutil.make_archive(
|
||||
temp_dir.joinpath(name),
|
||||
'zip',
|
||||
addons_dir,
|
||||
)
|
||||
checksum = generate_checksum(zipped_release)
|
||||
chechsum_name = name + ".zip.sha256"
|
||||
checksum_path = temp_dir / chechsum_name
|
||||
write_file(
|
||||
checksum_path,
|
||||
f"{checksum} {name}.zip",
|
||||
)
|
||||
return [Path(zipped_release), Path(checksum_path)]
|
||||
|
||||
|
||||
def write_file(file_path: Path, content):
|
||||
def write_file(file_path: Path, content: str) -> None:
|
||||
"""Write content to file at given file path.
|
||||
|
||||
Args:
|
||||
file_path (Path): Path to file to write to.
|
||||
content (str): Content to write to file.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
file = open(file_path, 'w')
|
||||
file.writelines(content)
|
||||
file.close()
|
||||
|
||||
|
||||
def replace_line(file_path: Path, new_line: str, line_number: int):
|
||||
file = open(
|
||||
file_path,
|
||||
)
|
||||
lines = file.readlines()
|
||||
lines[line_number] = new_line
|
||||
out = open(file_path, 'w')
|
||||
out.writelines(lines)
|
||||
out.close()
|
||||
def generate_checksum(archive_path: Path) -> str:
|
||||
"""Generate checksum for archive file.
|
||||
|
||||
Args:
|
||||
archive_path (Path): Path to archive file to generate checksum for.
|
||||
|
||||
def get_directory(repo_root: Path, folder_name: str) -> Path:
|
||||
"""Returns directory PATH, creates one if none exists"""
|
||||
path = repo_root.joinpath(folder_name)
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
return path
|
||||
|
||||
|
||||
def clean_str(string: str) -> str:
|
||||
"""Returns string with qoutes and line breaks removed"""
|
||||
return string.replace('\n', '').replace("'", "").replace('"', '')
|
||||
|
||||
|
||||
def generate_checksum(archive_path: str) -> str:
|
||||
"""
|
||||
Generate a checksum for a zip file
|
||||
Arguments:
|
||||
archive_path: String of the archive's file path
|
||||
Returns:
|
||||
sha256 checksum for the provided archive as string
|
||||
str: Hex digest string of checksum.
|
||||
"""
|
||||
sha256 = hashlib.sha256()
|
||||
|
||||
with open(archive_path, 'rb') as file:
|
||||
# Read the file in chunks to handle large files efficiently
|
||||
chunk = file.read(4096)
|
||||
while len(chunk) > 0:
|
||||
sha256.update(chunk)
|
||||
chunk = file.read(4096)
|
||||
return sha256.hexdigest()
|
||||
digest = hashlib.file_digest(file, "sha256")
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def changelog_category_get(changelog_messages: dict[str, str], title: str, key: str):
|
||||
"""
|
||||
Generate changelog messages for a specific category.
|
||||
Types are defined in global variable 'TYPES'
|
||||
Arguments:
|
||||
changelog_messages: dict contaning commit message & type
|
||||
title: Title of the changelog category
|
||||
key: Key for category/type as defined in global variable TYPES
|
||||
Returns:
|
||||
changelog entry for the given category/type as a string
|
||||
"""
|
||||
entry = ''
|
||||
if not any(commit for commit in changelog_messages if commit["type"] == key):
|
||||
return entry
|
||||
entry += f"### {title} \n"
|
||||
for commit in changelog_messages:
|
||||
if commit["type"] == key:
|
||||
entry += f'- {commit["message"]}'
|
||||
entry += "\n"
|
||||
return entry
|
||||
def send_delete_request(url) -> Response:
|
||||
response = requests.delete(url=f"{url}?token={API_TOKEN}")
|
||||
if response.status_code != 204:
|
||||
print(f"Error: {response.status_code}: '{response.reason}'")
|
||||
sys.exit(1)
|
||||
return response
|
||||
|
||||
|
||||
def changelog_generate(commit_hashes: list[str], version: str) -> str:
|
||||
"""
|
||||
Generate Changelog Entries from a list of commits hashes
|
||||
Arguments:
|
||||
commit_hashes: A list of commit hashes to include in Changelog
|
||||
version: Latest addon version number
|
||||
Returns:
|
||||
complete changelog for latest version as string
|
||||
"""
|
||||
|
||||
log_entry = f'## {version} - {datetime.date.today()} \n \n'
|
||||
changelog_messages = []
|
||||
if commit_hashes is not None:
|
||||
for commit in commit_hashes:
|
||||
message = (
|
||||
f"{cli_command(f'git log --pretty=format:%s -n 1 {commit}').stdout}\n"
|
||||
)
|
||||
changelog_messages.append(parse_commit(message))
|
||||
|
||||
for type in TYPES:
|
||||
log_entry += changelog_category_get(
|
||||
changelog_messages, TYPES.get(type).upper(), TYPES.get(type)
|
||||
)
|
||||
|
||||
log_entry += "### UN-CATEGORIZED \n"
|
||||
for commit in changelog_messages:
|
||||
if commit["message"] not in log_entry:
|
||||
log_entry += f"- {commit['message']}"
|
||||
log_entry += "\n"
|
||||
return log_entry
|
||||
def send_get_request(url: str) -> Response:
|
||||
response = requests.get(url=f"{url}?token={API_TOKEN}")
|
||||
if not (response.status_code == 200 or response.status_code == 404):
|
||||
print(f"Error: {response.status_code}: '{response.reason}'")
|
||||
sys.exit(1)
|
||||
return response
|
||||
|
||||
|
||||
def changelog_commits_get(directory: Path, commit_message: str) -> list[str]:
|
||||
"""
|
||||
Get list of commit hashes, that affect a given directory
|
||||
Arguments:
|
||||
directory: Name of directory/folder to filter commits
|
||||
commit_message: Prefix of commit to use as base for latest release
|
||||
Returns:
|
||||
list of commit hashes
|
||||
"""
|
||||
last_version_commit = None
|
||||
commits_in_folder = cli_command(
|
||||
f'git log --format=format:"%H" {directory}/*'
|
||||
).stdout.split('\n')
|
||||
# Find Last Version
|
||||
for commit in commits_in_folder:
|
||||
commit = clean_str(commit)
|
||||
commit_msg = cli_command(f'git log --format=%B -n 1 {commit}')
|
||||
if commit_message in commit_msg.stdout:
|
||||
last_version_commit = commit
|
||||
if last_version_commit is None:
|
||||
return
|
||||
|
||||
commits_since_release = cli_command(
|
||||
f'git rev-list {clean_str(last_version_commit)[0:9]}..HEAD'
|
||||
).stdout.split('\n')
|
||||
commit_hashes = []
|
||||
|
||||
for commit in commits_in_folder:
|
||||
if any(clean_str(commit) in x for x in commits_since_release):
|
||||
commit_hashes.append(clean_str(commit))
|
||||
return commit_hashes
|
||||
|
||||
|
||||
def changelog_file_write(file_path: Path, content: str):
|
||||
"""
|
||||
Append changelog to existing changelog file or create a new
|
||||
changelog file if none exists
|
||||
Arguments:
|
||||
file_path: PATH to changelog
|
||||
content: changelog for latest version as string
|
||||
"""
|
||||
if file_path.exists():
|
||||
dummy_file = str(file_path._str) + '.bak'
|
||||
with open(file_path, 'r') as read_obj, open(dummy_file, 'w') as write_obj:
|
||||
write_obj.write(content)
|
||||
for line in read_obj:
|
||||
write_obj.write(line)
|
||||
os.remove(file_path)
|
||||
os.rename(dummy_file, file_path)
|
||||
else:
|
||||
write_file(file_path, content)
|
||||
return file_path
|
||||
|
||||
|
||||
def update_release_table(addon_dir: Path, version: str, release_version: str):
|
||||
directory = Path(__file__).parent
|
||||
template_file = directory.joinpath("overview.md.template")
|
||||
table_file = directory.joinpath("overview.md")
|
||||
with open(template_file, 'r') as readme_template:
|
||||
for num, line in enumerate(readme_template):
|
||||
if addon_dir.name in line:
|
||||
line_to_replace = num
|
||||
break # Use first line found
|
||||
line = line.replace("<VERSION>", f"{version}")
|
||||
line = line.replace(
|
||||
"<ZIP_URL>",
|
||||
f"{base_url}{repo_path}/releases/download/{release_version}/{addon_dir.name}-{version}.zip",
|
||||
)
|
||||
new_line = line.replace(
|
||||
"<CHECKSUM_URL>",
|
||||
f"{base_url}{repo_path}/releases/download/{release_version}/{addon_dir.name}-{version}.sha256",
|
||||
)
|
||||
replace_line(table_file, new_line, line_to_replace)
|
||||
return table_file
|
||||
|
||||
|
||||
def addon_package(
|
||||
directory: Path,
|
||||
commit_prefix: str,
|
||||
is_major=False,
|
||||
force=False,
|
||||
test=False,
|
||||
output_path=None,
|
||||
to_upload=[],
|
||||
release_version="",
|
||||
):
|
||||
"""
|
||||
For a give directory, if new commits are found after the commit matching 'commit_prefix',
|
||||
bump addon version, generate a changelog, commit changes and package addon into an archive.
|
||||
Print statements indicate if addon was version bumped, or if new version was found.
|
||||
Arguments:
|
||||
directory: Name of directory/folder to filter commits
|
||||
commit_prefix: Prefix of commit to use as base for latest release
|
||||
is_major: if major 2nd digit in version is updated, else 3rd digit
|
||||
"""
|
||||
commit_msg = 'Version Bump:' if commit_prefix is None else commit_prefix
|
||||
commits_in_folder = changelog_commits_get(directory, commit_msg)
|
||||
dist_dir = get_directory(REPO_ROOT_DIR, "dist")
|
||||
|
||||
if commits_in_folder or force:
|
||||
init_file, version = addon_version_bump(directory, is_major)
|
||||
change_log = changelog_generate(commits_in_folder, version)
|
||||
table_file = update_release_table(directory, version, release_version)
|
||||
change_log_file = changelog_file_write(
|
||||
directory.joinpath("CHANGELOG.md"), change_log
|
||||
)
|
||||
if not test:
|
||||
cli_command(f'git reset')
|
||||
cli_command(f'git stage {change_log_file}')
|
||||
cli_command(f'git stage {init_file}')
|
||||
cli_command(f'git stage {table_file}')
|
||||
subprocess.run(
|
||||
['git', 'commit', '-m', f"Version Bump: {directory.name} {version}"],
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"Version Bump: {directory.name} {version}")
|
||||
name = directory.name
|
||||
if output_path is None:
|
||||
addon_output_dir = get_directory(dist_dir, directory.name)
|
||||
else:
|
||||
addon_output_dir = get_directory(Path(output_path), directory.name)
|
||||
|
||||
zipped_addon = shutil.make_archive(
|
||||
addon_output_dir.joinpath(f"{name}-{version}"),
|
||||
'zip',
|
||||
directory.parent,
|
||||
directory.name,
|
||||
)
|
||||
checksum = generate_checksum(zipped_addon)
|
||||
checksum_path = addon_output_dir.joinpath(f"{name}-{version}.sha256")
|
||||
checksum_file = write_file(
|
||||
checksum_path,
|
||||
f"{checksum} {name}-{version}.zip",
|
||||
)
|
||||
to_upload.append(zipped_addon)
|
||||
to_upload.append(checksum_path._str)
|
||||
else:
|
||||
print(f"No New Version: {directory.name}")
|
||||
|
||||
|
||||
def addon_version_set(version_line: str, is_major: bool) -> str:
|
||||
"""
|
||||
Read bl_info within addon's __init__.py file to get new version number
|
||||
Arguments:
|
||||
version_line: Line of bl_info containing version number
|
||||
is_major: if major 2nd digit in version is updated, else 3rd digit
|
||||
Returns
|
||||
Latest addon version number
|
||||
"""
|
||||
version = version_line.split('(')[1].split(')')[0]
|
||||
# Bump either last digit for minor versions and second last digit for major
|
||||
if is_major:
|
||||
new_version = version[:-4] + str(int(version[3]) + 1) + version[-3:]
|
||||
else:
|
||||
new_version = version[:-1] + str(int(version[-1]) + 1)
|
||||
return new_version
|
||||
|
||||
|
||||
def addon_version_bump(directory: Path, is_major: bool):
|
||||
"""
|
||||
Update bl_info within addon's __init__.py file to indicate
|
||||
version bump. Expects line to read as '"version": (n, n, n),\n'
|
||||
Arguments:
|
||||
directory: Name of directory/folder containing addon
|
||||
is_major: if major 2nd digit in version is updated, else 3rd digit
|
||||
|
||||
Returns:
|
||||
init_file: PATH to init file that has been updated with new version
|
||||
version: Latest addon version number
|
||||
"""
|
||||
|
||||
version_line = None
|
||||
str_find = "version"
|
||||
init_file = directory.joinpath("__init__.py")
|
||||
with open(init_file, 'r') as myFile:
|
||||
for num, line in enumerate(myFile):
|
||||
if str_find in line and "(" in line and line[0] != "#":
|
||||
version_line = num
|
||||
break # Use first line found
|
||||
|
||||
file = open(
|
||||
init_file,
|
||||
)
|
||||
lines = file.readlines()
|
||||
version = addon_version_set(lines[version_line], is_major)
|
||||
repl_str = f' "version": ({version}),\n'
|
||||
replace_line(init_file, repl_str, version_line)
|
||||
return init_file, version.replace(', ', '.').replace(',', '.')
|
||||
|
||||
|
||||
### GITEA UPLOAD RELEASE
|
||||
import requests # TODO ADD PRINT STATEMENT IF UNABLE TO IMPORT
|
||||
import json
|
||||
|
||||
"""
|
||||
API token must be created under user>settings>application
|
||||
- Use browser to 'INSPECT' the Generate Token button
|
||||
- Find the property 'GTHidden Display' and remove the element of 'None' to nothing
|
||||
- Then Set the correct scope for the key using the new dropdown menu before creating tag
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def upload_file_to_release(url, api_token, release_id, file):
|
||||
file_name = Path(file.name).name
|
||||
file_content = [
|
||||
('attachment', (file_name, file, 'application/zip')),
|
||||
]
|
||||
response = requests.post(
|
||||
url=f"{url}/{release_id}/assets?name={file_name}&token={api_token}",
|
||||
files=file_content,
|
||||
)
|
||||
if not response.status_code == 201:
|
||||
print(f"{file_name} failed to upload")
|
||||
else:
|
||||
print(f"Uploaded {file_name}")
|
||||
|
||||
|
||||
def send_post_request(url, api_token, data):
|
||||
def send_post_request(url: str, data: dict) -> Response:
|
||||
header_cont = {
|
||||
'Content-type': 'application/json',
|
||||
}
|
||||
response = requests.post(
|
||||
url=f"{url}?token={api_token}",
|
||||
url=f"{url}?token={API_TOKEN}",
|
||||
headers=header_cont,
|
||||
data=json.dumps(data),
|
||||
)
|
||||
response_json = response.json()
|
||||
if response.status_code != 201:
|
||||
print(response_json["message"])
|
||||
return response_json
|
||||
|
||||
|
||||
def create_new_release(tag_url, base_release_url, release_version, api_token):
|
||||
release_description = "Latest Release of Blender Studio Pipeline"
|
||||
# Create New Tag
|
||||
tag_content = {
|
||||
"message": f"{release_description}",
|
||||
"tag_name": f"{release_version}",
|
||||
"target": f"main",
|
||||
}
|
||||
|
||||
send_post_request(tag_url, api_token, tag_content)
|
||||
|
||||
# Create New Release
|
||||
release_content = {
|
||||
"body": f"{release_description}",
|
||||
"draft": False,
|
||||
"name": f"Pipeline Release {release_version}",
|
||||
"prerelease": False,
|
||||
"tag_name": f"{release_version}",
|
||||
"target_commitish": "string", # will default to latest
|
||||
}
|
||||
|
||||
return send_post_request(base_release_url, api_token, release_content)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parser.parse_args()
|
||||
commit = args.commit
|
||||
major = args.major
|
||||
test = args.test
|
||||
user_names = args.name
|
||||
output_path = args.output
|
||||
force = args.force
|
||||
reuse_latest_relase = args.reuse_lastest_release
|
||||
addon_folder = REPO_ROOT_DIR.joinpath(REPO_ROOT_DIR, "scripts-blender/addons")
|
||||
addon_to_upload = []
|
||||
base_release_url = f"{api_path}{release_path}"
|
||||
base_tag_url = f"{api_path}{tag_path}"
|
||||
latest_release = requests.get(url=f"{base_release_url}/latest?token={api_token}")
|
||||
# Exception for intial release
|
||||
if latest_release.status_code == 404:
|
||||
release_version = '0.0.1'
|
||||
else:
|
||||
latest_tag = latest_release.json()["tag_name"]
|
||||
release_version = latest_tag.replace(
|
||||
latest_tag[-1], str(int(latest_tag[-1]) + 1)
|
||||
)
|
||||
|
||||
addon_dirs = [
|
||||
name
|
||||
for name in os.listdir(addon_folder)
|
||||
if os.path.isdir(addon_folder.joinpath(name))
|
||||
]
|
||||
if user_names:
|
||||
addon_dirs = [
|
||||
name
|
||||
for name in os.listdir(addon_folder)
|
||||
if os.path.isdir(addon_folder.joinpath(name)) and name in user_names
|
||||
]
|
||||
|
||||
for dir in addon_dirs:
|
||||
addon_to_package = addon_folder.joinpath(addon_folder, dir)
|
||||
addon_package(
|
||||
addon_to_package,
|
||||
commit,
|
||||
major,
|
||||
force,
|
||||
test,
|
||||
output_path,
|
||||
addon_to_upload,
|
||||
release_version,
|
||||
)
|
||||
|
||||
if not test:
|
||||
# Release Script
|
||||
if reuse_latest_relase:
|
||||
release_id = latest_release.json()["id"]
|
||||
else:
|
||||
response = create_new_release(
|
||||
base_tag_url, base_release_url, release_version, api_token
|
||||
)
|
||||
release_id = response["id"]
|
||||
|
||||
for file in addon_to_upload:
|
||||
payload = open(file, 'rb')
|
||||
upload_file_to_release(
|
||||
base_release_url,
|
||||
api_token,
|
||||
release_id,
|
||||
payload,
|
||||
)
|
||||
return 0
|
||||
sys.exit(1)
|
||||
return response
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -1,10 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import glob
|
||||
import hashlib
|
||||
import os
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
from urllib.request import urlretrieve
|
||||
import sys
|
||||
import requests
|
||||
import glob
|
||||
import os
|
||||
|
||||
|
||||
def update_blender_studio_addons(download_folder_path: Path):
|
||||
if not download_folder_path.exists():
|
||||
print(
|
||||
f"Ensure script is run out of Project Tools directory {str(download_folder_path)} does not exist"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
sha_file = download_folder_path.joinpath("blender_studio_add-ons_latest.zip.sha256")
|
||||
zip_file = download_folder_path.joinpath("blender_studio_add-ons_latest.zip")
|
||||
|
||||
url_sha = "https://projects.blender.org/studio/blender-studio-pipeline/releases/download/latest/blender_studio_add-ons_latest.zip.sha256"
|
||||
url_zip = "https://projects.blender.org/studio/blender-studio-pipeline/releases/download/latest/blender_studio_add-ons_latest.zip"
|
||||
|
||||
# Check current sha and early return if match
|
||||
web_sha = requests.get(url_sha).text.strip().lower()
|
||||
if sha_file.exists() & zip_file.exists():
|
||||
if shasum_matches(zip_file, web_sha):
|
||||
print(f"{zip_file.name} already up to date, canceling update")
|
||||
return
|
||||
else:
|
||||
# Remove current files
|
||||
if sha_file.exists():
|
||||
sha_file.unlink()
|
||||
if zip_file.exists():
|
||||
zip_file.unlink()
|
||||
|
||||
print(f"Downloading {zip_file.name}......", end="")
|
||||
urlretrieve(url_zip, str(zip_file))
|
||||
print("Complete")
|
||||
print(f"Downloading {sha_file.name}......", end="")
|
||||
urlretrieve(url_sha, str(sha_file))
|
||||
print("Complete")
|
||||
|
||||
if not shasum_matches(zip_file, web_sha):
|
||||
print(f"Downloaded file {zip_file.name} does not match its shasum, exiting!")
|
||||
exit(1)
|
||||
|
||||
print("Blender Studio Add-Ons Successfully Updated for Current Project")
|
||||
print(
|
||||
"Blender Studio Add-Ons will be copied to your local directory next time you launch Blender via Projet Tools"
|
||||
)
|
||||
|
||||
|
||||
def shasum_matches(file, sha_sum):
|
||||
with open(file, "rb") as f:
|
||||
digest = hashlib.file_digest(f, "sha256")
|
||||
return sha_sum.startswith(digest.hexdigest())
|
||||
|
||||
|
||||
def download_file(url, out_folder, filename):
|
||||
@ -33,16 +84,13 @@ def download_file(url, out_folder, filename):
|
||||
return local_filename
|
||||
|
||||
|
||||
current_file_folder_path = pathlib.Path(__file__).parent
|
||||
current_file_folder_path = Path(__file__).parent
|
||||
download_folder_path = (current_file_folder_path / "../../shared/artifacts/addons/").resolve()
|
||||
update_blender_studio_addons(download_folder_path)
|
||||
|
||||
# Ensure that the download directory exists
|
||||
os.makedirs(download_folder_path, exist_ok=True)
|
||||
|
||||
print("This script currently does nothing. If you want to update the 'studio-pipeline' addons, run the 'package_local.py' script in the studio-pipline repo.")
|
||||
|
||||
#download_file(
|
||||
# Customize this script to download add-ons from other sources
|
||||
# download_file(
|
||||
# "https://projects.blender.org/studio/blender-studio-pipeline/archive/main.zip",
|
||||
# download_folder_path,
|
||||
# "blender-studio-pipeline-main.zip",
|
||||
#)
|
||||
# )
|
||||
|
Loading…
Reference in New Issue
Block a user