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:
Ken Van Hoeylandt
2025-10-18 15:40:59 +02:00
committed by GitHub
parent bb0d536baa
commit 05862ae65c
9 changed files with 183 additions and 16 deletions
+9 -9
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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)