diff --git a/.github/actions/publish-apps/action.yml b/.github/actions/publish-apps/action.yml new file mode 100644 index 0000000..e9d7cac --- /dev/null +++ b/.github/actions/publish-apps/action.yml @@ -0,0 +1,21 @@ +name: Publish Apps + +inputs: + sdk_version: + description: The SDK version that determines the path on the CDN + required: true + +runs: + using: 'composite' + steps: + - name: 'Download cdn-files' + uses: actions/download-artifact@v4 + with: + name: 'cdn-files' + path: cdn_files + - name: 'Install boto3' + shell: bash + run: pip install boto3 + - name: 'Upload files' + shell: bash + run: python Buildscripts/CDN/upload-app-files.py cdn_files ${{ inputs.sdk_version }} ${{ env.CDN_ID }} ${{ env.CDN_TOKEN_NAME }} ${{ env.CDN_TOKEN_VALUE }} diff --git a/.github/actions/release-apps/action.yml b/.github/actions/release-apps/action.yml index 9c9b715..0b4b08e 100644 --- a/.github/actions/release-apps/action.yml +++ b/.github/actions/release-apps/action.yml @@ -1,5 +1,10 @@ name: Release Apps +outputs: + sdk_version: + description: 'Common SDK version shared by all bundled apps' + value: ${{ steps.release.outputs.sdk_version }} + runs: using: 'composite' steps: @@ -11,8 +16,11 @@ runs: run: rsync -av downloaded_apps/*/*.app cdn_files/ shell: bash - name: 'Create CDN release files' - run: python release.py cdn_files/ + id: release shell: bash + run: | + python release.py cdn_files/ + echo "sdk_version=$(cat sdk_version.txt)" >> "$GITHUB_OUTPUT" - name: 'Upload Artifact' uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9906cbf..559827f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: "Build" uses: ./.github/actions/build-app with: @@ -23,7 +25,26 @@ jobs: Bundle: runs-on: ubuntu-latest needs: [Build] + outputs: + sdk_version: ${{ steps.release.outputs.sdk_version }} steps: - uses: actions/checkout@v4 - name: "Build" + id: release uses: ./.github/actions/release-apps + PublishApps: + runs-on: ubuntu-latest + needs: [Bundle] + if: (github.event_name == 'push' && github.ref == 'refs/heads/main') + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: "Publish Apps" + env: + CDN_ID: ${{ secrets.CDN_ID }} + CDN_TOKEN_NAME: ${{ secrets.CDN_TOKEN_NAME }} + CDN_TOKEN_VALUE: ${{ secrets.CDN_TOKEN_VALUE }} + uses: ./.github/actions/publish-apps + with: + sdk_version: ${{ needs.Bundle.outputs.sdk_version }} diff --git a/Buildscripts/CDN/upload-app-files.py b/Buildscripts/CDN/upload-app-files.py new file mode 100644 index 0000000..eab6986 --- /dev/null +++ b/Buildscripts/CDN/upload-app-files.py @@ -0,0 +1,70 @@ +import os +import sys +import boto3 + +SHELL_COLOR_RED = "\033[91m" +SHELL_COLOR_ORANGE = "\033[93m" +SHELL_COLOR_RESET = "\033[m" + +def print_warning(message): + print(f"{SHELL_COLOR_ORANGE}WARNING: {message}{SHELL_COLOR_RESET}") + +def print_error(message): + print(f"{SHELL_COLOR_RED}ERROR: {message}{SHELL_COLOR_RESET}") + +def print_help(): + print("Usage: python upload-app-files.py [path] [sdkVersion] [cloudflareAccountId] [cloudflareTokenName] [cloudflareTokenValue]") + print("") + print("Options:") + print(" --index-only Upload only apps.json") + +def exit_with_error(message): + print_error(message) + sys.exit(1) + +def main(path: str, sdk_version: str, cloudflare_account_id, cloudflare_token_name: str, cloudflare_token_value: str, index_only: bool): + if not os.path.exists(path): + exit_with_error(f"Path not found: {path}") + s3 = boto3.client( + service_name="s3", + endpoint_url=f"https://{cloudflare_account_id}.r2.cloudflarestorage.com", + aws_access_key_id=cloudflare_token_name, + aws_secret_access_key=cloudflare_token_value, + region_name="auto" + ) + files_to_upload = os.listdir(path) + if index_only: + files_to_upload = [f for f in files_to_upload if f == 'apps.json'] + else: + # Ensure apps.json is uploaded last so it never references files that + # haven't finished uploading yet. + files_to_upload.sort(key=lambda f: f == 'apps.json') + counter = 1 + total = len(files_to_upload) + for file_name in files_to_upload: + object_path = f"apps/{sdk_version}/{file_name}" + print(f"[{counter}/{total}] Uploading {file_name} to {object_path}") + file_path = os.path.join(path, file_name) + try: + s3.upload_file(file_path, "tactility", object_path) + except Exception as e: + exit_with_error(f"Failed to upload {file_name}: {str(e)}") + counter += 1 + +if __name__ == "__main__": + print("Tactility CDN Apps Uploader") + if "--help" in sys.argv: + print_help() + sys.exit() + # Argument validation + if len(sys.argv) < 6: + print_help() + sys.exit(1) + main( + path=sys.argv[1], + sdk_version=sys.argv[2], + cloudflare_account_id=sys.argv[3], + cloudflare_token_name=sys.argv[4], + cloudflare_token_value=sys.argv[5], + index_only="--index-only" in sys.argv + ) diff --git a/release.py b/release.py index 9e7e47b..4456f5f 100644 --- a/release.py +++ b/release.py @@ -1,8 +1,10 @@ import json +import subprocess import tarfile import os import tempfile import sys +from datetime import datetime, UTC def read_properties_file(path): properties = {} @@ -81,6 +83,17 @@ def get_os_version(manifest): else: return sdk +def check_and_get_sdk_version(manifest_map): + """Ensure all apps target the same (simplified) SDK version and return it.""" + versions = {get_os_version(manifest) for manifest in manifest_map.values()} + if len(versions) != 1: + print(f"ERROR: Apps target multiple SDK versions: {sorted(versions)}. All apps must target the same SDK version.") + sys.exit(1) + return next(iter(versions)) + +def get_git_commit_hash(): + return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip() + def manifest_config_to_flat_json(manifest): """Convert a flat (V2) manifest dict into a flat JSON-like dict. @@ -136,15 +149,23 @@ def write_json(filepath, data): sys.exit() app_directory = sys.argv[1] manifest_map = {} - output_json = { - "apps": [] - } any_manifest = None if os.path.exists(app_directory): for file in os.listdir(app_directory): if file.endswith(".app"): file_path = os.path.join(app_directory, file) manifest_map[file_path] = get_manifest(file_path) + # All bundled apps must target the same SDK version; this becomes the CDN path segment + sdk_version = check_and_get_sdk_version(manifest_map) + with open("sdk_version.txt", "w") as f: + f.write(sdk_version) + print(f"SDK version: {sdk_version}") + output_json = { + "sdkVersion": sdk_version, + "created": datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'), + "gitCommit": get_git_commit_hash(), + "apps": [] + } # Rename files and collect manifest data into output json object for file_path in manifest_map.keys(): print(f"Processing {file_path}: {manifest_map[file_path]}") @@ -158,4 +179,4 @@ def write_json(filepath, data): any_manifest = manifest # Write JSON output_json_path = os.path.join(app_directory, "apps.json") - write_json(output_json_path, output_json) \ No newline at end of file + write_json(output_json_path, output_json)