Release pipelines (#12)
Create a release pipeline that builds a zip file with everything to copy onto the CDN for delivery in the upcoming App Hub.
This commit is contained in:
committed by
GitHub
parent
bb0d536baa
commit
05862ae65c
@@ -1,21 +1,21 @@
|
||||
name: Release Apps
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Download app artifacts
|
||||
- name: 'Download app artifacts'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: downloaded_apps
|
||||
- name: Place reports' artifacts
|
||||
run: rsync -av downloaded_apps/*/*/ release_apps/
|
||||
- name: Copy apps to cdn_files/
|
||||
run: rsync -av downloaded_apps/*/*.app cdn_files/
|
||||
shell: bash
|
||||
- name: 'Create CDN release files'
|
||||
run: python release.py cdn_files/
|
||||
shell: bash
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "apps"
|
||||
path: release_apps/
|
||||
name: 'cdn-files'
|
||||
path: cdn_files/
|
||||
retention-days: 30
|
||||
- name: Output
|
||||
run: ls -R release_apps/
|
||||
shell: bash
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Build Apps
|
||||
name: Main
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -1,7 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.6.0-SNAPSHOT11
|
||||
sdk=0.6.0-SNAPSHOT13
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=one.tactility.calculator
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.6.0-SNAPSHOT11
|
||||
sdk=0.6.0-SNAPSHOT13
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=one.tactility.diceware
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.6.0-SNAPSHOT11
|
||||
sdk=0.6.0-SNAPSHOT13
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=one.tactility.gpio
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.6.0-SNAPSHOT11
|
||||
sdk=0.6.0-SNAPSHOT13
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=one.tactility.graphicsdemo
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.6.0-SNAPSHOT11
|
||||
sdk=0.6.0-SNAPSHOT13
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=one.tactility.helloworld
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.6.0-SNAPSHOT11
|
||||
sdk=0.6.0-SNAPSHOT13
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=one.tactility.serialconsole
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
import json
|
||||
import tarfile
|
||||
import os
|
||||
import tempfile
|
||||
import configparser
|
||||
import sys
|
||||
|
||||
def read_properties_file(path):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(path)
|
||||
return config
|
||||
|
||||
def get_manifest(appPath):
|
||||
"""Extract only the file named 'manifest.properties' from the given tar/tar.gz
|
||||
archive into the directory specified by outputPath.
|
||||
|
||||
Returns True on success, False on failure.
|
||||
"""
|
||||
print(f"Unpacking manifest.properties from {appPath}")
|
||||
if not os.path.isfile(appPath):
|
||||
print(f"File not found: {appPath}")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Auto-detect compression (tar, tar.gz, tar.bz2, etc.)
|
||||
with tarfile.open(appPath, mode="r:*") as tf:
|
||||
# Find a member whose base name is exactly 'manifest.properties'
|
||||
member = None
|
||||
for m in tf.getmembers():
|
||||
# Skip directories and non-files
|
||||
if not m.isfile():
|
||||
continue
|
||||
if os.path.basename(m.name) == "manifest.properties":
|
||||
member = m
|
||||
break
|
||||
|
||||
if member is None:
|
||||
print("manifest.properties not found in archive")
|
||||
return None
|
||||
|
||||
# Read the member's content and write it directly into outputPath
|
||||
# Create a temporary directory that persists after this function returns.
|
||||
temp_dir = tempfile.mkdtemp(prefix="tactility_app_")
|
||||
dest_file = os.path.join(temp_dir, "manifest.properties")
|
||||
with tf.extractfile(member) as src, open(dest_file, "wb") as dst:
|
||||
if src is None:
|
||||
print("Failed to open manifest.properties from the archive")
|
||||
return False
|
||||
while True:
|
||||
chunk = src.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
dst.write(chunk)
|
||||
properties = read_properties_file(dest_file)
|
||||
os.remove(dest_file)
|
||||
os.rmdir(temp_dir)
|
||||
return properties
|
||||
except tarfile.ReadError as e:
|
||||
print(f"Not a valid tar archive: {appPath} ({e})")
|
||||
except Exception as e:
|
||||
print(f"Failed to extract manifest.properties from {appPath}: {e}")
|
||||
return None
|
||||
|
||||
def get_versioned_file_name(manifest):
|
||||
app_id = manifest["app"]["id"]
|
||||
version_code = manifest["app"]["versionCode"]
|
||||
return f"{app_id}-{version_code}.app"
|
||||
|
||||
def get_os_version(manifest):
|
||||
sdk = manifest["target"]["sdk"]
|
||||
# Remove trailing hyphen suffix if present
|
||||
if "-" in sdk:
|
||||
return sdk.rsplit("-", 1)[0].strip()
|
||||
else:
|
||||
return sdk
|
||||
|
||||
def manifest_config_to_flat_json(manifest):
|
||||
"""Convert a ConfigParser manifest into a flat JSON-like dict.
|
||||
|
||||
Expected sections/keys (case-insensitive for keys):
|
||||
- [app]
|
||||
id -> appId
|
||||
versionName -> appVersionName
|
||||
versionCode -> appVersionCode (int)
|
||||
name -> appName
|
||||
description -> appDescription (optional; default "")
|
||||
- [target]
|
||||
sdk -> targetSdk
|
||||
platforms -> targetPlatforms (comma-separated list)
|
||||
|
||||
Unknown/missing values fall back to sensible defaults per requirements.
|
||||
"""
|
||||
def get_opt(section, option, default=None):
|
||||
if not manifest.has_section(section):
|
||||
return default
|
||||
# try exact option then lowercase (RawConfigParser lowercases by default)
|
||||
if manifest.has_option(section, option):
|
||||
return manifest.get(section, option)
|
||||
low = option.lower()
|
||||
if manifest.has_option(section, low):
|
||||
return manifest.get(section, low)
|
||||
return default
|
||||
|
||||
# Map values
|
||||
app_id = get_opt("app", "id", "")
|
||||
app_version_name = get_opt("app", "versionName", "")
|
||||
app_version_code_raw = get_opt("app", "versionCode", "0")
|
||||
app_name = get_opt("app", "name", "")
|
||||
app_description = get_opt("app", "description", "") or ""
|
||||
|
||||
# Coerce version code to int safely
|
||||
try:
|
||||
app_version_code = int(str(app_version_code_raw).strip())
|
||||
except Exception:
|
||||
app_version_code = 0
|
||||
|
||||
target_sdk = get_opt("target", "sdk", "")
|
||||
platforms_raw = get_opt("target", "platforms", "")
|
||||
target_platforms = [p.strip() for p in str(platforms_raw).split(",") if p.strip()] if platforms_raw is not None else []
|
||||
|
||||
filename = get_versioned_file_name(manifest)
|
||||
return {
|
||||
"appId": app_id,
|
||||
"appVersionName": app_version_name,
|
||||
"appVersionCode": app_version_code,
|
||||
"appName": app_name,
|
||||
"appDescription": app_description,
|
||||
"targetSdk": target_sdk,
|
||||
"targetPlatforms": target_platforms,
|
||||
"file": filename
|
||||
}
|
||||
|
||||
def write_json(filepath, data):
|
||||
text = json.dumps(data)
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(text)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Tactility app release script")
|
||||
if len(sys.argv) != 2:
|
||||
print("Missing argument")
|
||||
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)
|
||||
# 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]}")
|
||||
manifest = manifest_map[file_path]
|
||||
manifest_json = manifest_config_to_flat_json(manifest)
|
||||
output_json["apps"].append(manifest_json)
|
||||
release_file_name = get_versioned_file_name(manifest)
|
||||
renamed_file_path = os.path.join(app_directory, release_file_name)
|
||||
os.rename(file_path, renamed_file_path)
|
||||
print(f"Renamed {file_path} to {renamed_file_path}")
|
||||
any_manifest = manifest
|
||||
# Write JSON
|
||||
output_json_path = os.path.join(app_directory, "apps.json")
|
||||
write_json(output_json_path, output_json)
|
||||
Reference in New Issue
Block a user