feat(workout): give created apps their release command
Write a standalone hemx-workout command into created Workout apps so new app owners keep the same dev, test, build, mobile-release, mobile-verify, and doctor surface outside the monorepo. req: examples/006
This commit is contained in:
+120
-1
@@ -151,9 +151,10 @@ fn create_workout_app(destination: &Path) -> std::io::Result<()> {
|
||||
"hemx_workout_example",
|
||||
"hemx_workout_app",
|
||||
)?;
|
||||
write_workout_app_command(destination)?;
|
||||
fs::write(
|
||||
destination.join("CREATED.md"),
|
||||
"# Created Workout app\n\nRun locally with:\n\n```sh\ncargo run --manifest-path Cargo.toml --bin workout-app\n```\n\nBuild for production with:\n\n```sh\ncargo build --manifest-path Cargo.toml --release --bin workout-app\n```\n\nThis app owns command/event/projection state and keeps Android/iOS signing outside the repo. req: examples/006\n",
|
||||
"# Created Workout app\n\nUse one command surface from this directory:\n\n```sh\n./hemx-workout dev\n./hemx-workout test\n./hemx-workout build\nHEMX_WORKOUT_ORIGIN=https://workout.example.com ./hemx-workout mobile-release\nHEMX_WORKOUT_ORIGIN=https://workout.example.com ./hemx-workout mobile-verify\n./hemx-workout doctor\n```\n\nThis app owns command/event/projection state and keeps Android/iOS SDKs, store submission targets, and signing outside the repo. req: examples/006\n",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -174,6 +175,119 @@ fn replace_in_file(path: &Path, from: &str, to: &str) -> std::io::Result<()> {
|
||||
fs::write(path, contents.replace(from, to))
|
||||
}
|
||||
|
||||
fn write_workout_app_command(destination: &Path) -> std::io::Result<()> {
|
||||
let script_path = destination.join("hemx-workout");
|
||||
fs::write(&script_path, workout_app_command_script())?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut permissions = fs::metadata(&script_path)?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(&script_path, permissions)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn workout_app_command_script() -> String {
|
||||
format!(
|
||||
r#"#!/usr/bin/env sh
|
||||
set -eu
|
||||
APP_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
CMD=${{1:-dev}}
|
||||
OUT=${{HEMX_WORKOUT_MOBILE_OUT:-"$APP_DIR/target/hemx-mobile/workout"}}
|
||||
ORIGIN=${{HEMX_WORKOUT_ORIGIN:-https://workout.example.invalid}}
|
||||
APP_ID=${{HEMX_WORKOUT_APP_ID:-com.hemx.workout}}
|
||||
APP_NAME=${{HEMX_WORKOUT_APP_NAME:-hemx Workout Copilot}}
|
||||
VERSION=${{HEMX_WORKOUT_VERSION:-0.1.0}}
|
||||
RUNTIME_PATH='{runtime_path}'
|
||||
RUNTIME_SHA='{runtime_sha}'
|
||||
|
||||
write_blockers() {{
|
||||
mkdir -p "$OUT"
|
||||
{{
|
||||
echo '# Workout mobile external blockers'
|
||||
echo
|
||||
echo 'The release kit is generated, but these external inputs are still required for signed store artifacts:'
|
||||
echo
|
||||
[ -n "${{ANDROID_HOME:-${{ANDROID_SDK_ROOT:-}}}}" ] || echo '- Android SDK not found: set ANDROID_HOME or ANDROID_SDK_ROOT before producing signed Android artifacts'
|
||||
command -v java >/dev/null 2>&1 || echo '- Java runtime not found: Android packaging requires a JDK'
|
||||
[ -n "${{HEMX_WORKOUT_ANDROID_KEYSTORE:-}}" ] || echo '- Android signing key not configured: set HEMX_WORKOUT_ANDROID_KEYSTORE for store-ready signing'
|
||||
[ -n "${{HEMX_WORKOUT_GOOGLE_PLAY_TRACK:-}}" ] || echo '- Google Play submission target not configured: set HEMX_WORKOUT_GOOGLE_PLAY_TRACK after choosing the Play Console track outside this app'
|
||||
command -v xcodebuild >/dev/null 2>&1 || echo '- Xcode command line tools not found: iOS archive/export requires xcodebuild on macOS'
|
||||
[ -n "${{HEMX_WORKOUT_IOS_TEAM_ID:-}}" ] || echo '- iOS signing team not configured: set HEMX_WORKOUT_IOS_TEAM_ID for App Store/TestFlight export'
|
||||
[ -n "${{HEMX_WORKOUT_APP_STORE_CONNECT_TEAM:-}}" ] || echo '- App Store Connect submission team not configured: set HEMX_WORKOUT_APP_STORE_CONNECT_TEAM after choosing the Apple account outside this app'
|
||||
}} > "$OUT/BLOCKERS.md"
|
||||
}}
|
||||
|
||||
case "$CMD" in
|
||||
dev|run)
|
||||
exec cargo run --manifest-path "$APP_DIR/Cargo.toml" --bin workout-app
|
||||
;;
|
||||
test)
|
||||
exec cargo test --manifest-path "$APP_DIR/Cargo.toml"
|
||||
;;
|
||||
build)
|
||||
exec cargo build --manifest-path "$APP_DIR/Cargo.toml" --release --bin workout-app
|
||||
;;
|
||||
mobile-release)
|
||||
cargo build --manifest-path "$APP_DIR/Cargo.toml" --release --bin workout-app
|
||||
mkdir -p "$OUT/android" "$OUT/ios"
|
||||
write_blockers
|
||||
printf 'path\talgorithm\tdigest\n%s\tsha256\t%s\n' "$RUNTIME_PATH" "$RUNTIME_SHA" > "$OUT/asset-integrity.tsv"
|
||||
cat > "$OUT/release-manifest.json" <<EOF
|
||||
{{
|
||||
"app_id": "$APP_ID",
|
||||
"name": "$APP_NAME",
|
||||
"version": "$VERSION",
|
||||
"origin": "$ORIGIN",
|
||||
"server_binary": "$APP_DIR/target/release/workout-app",
|
||||
"runtime_asset_path": "$RUNTIME_PATH",
|
||||
"runtime_asset_sha256": "$RUNTIME_SHA",
|
||||
"asset_integrity": "asset-integrity.tsv",
|
||||
"cache_policy": "cache only release-scoped HTML/CSS/runtime assets; never store DOM patches or UI effects as truth",
|
||||
"state_policy": "app-owned command/event/projection records",
|
||||
"host_result_kinds": ["denied", "timeout", "unavailable", "error"],
|
||||
"environment_boundary": "public mobile shell config lives here; secrets and signing credentials stay outside the app repo",
|
||||
"rollback": "redeploy the previous server binary and matching mobile shell metadata; rebuild store artifacts with the previous version/signing inputs",
|
||||
"android": "android/twa-release.json",
|
||||
"ios": "ios/webview-release.json"
|
||||
}}
|
||||
EOF
|
||||
cat > "$OUT/android/twa-release.json" <<EOF
|
||||
{{"package":"$APP_ID","name":"$APP_NAME","start_url":"$ORIGIN/","host_capabilities":["share","haptics"],"host_result_kinds":["denied","timeout","unavailable","error"],"signing":"external Android keystore; never commit credentials"}}
|
||||
EOF
|
||||
cat > "$OUT/ios/webview-release.json" <<EOF
|
||||
{{"bundle_id":"$APP_ID","name":"$APP_NAME","start_url":"$ORIGIN/","host_capabilities":["share","haptics"],"host_result_kinds":["denied","timeout","unavailable","error"],"signing":"external Apple team/provisioning profile; never commit credentials"}}
|
||||
EOF
|
||||
echo "workout-mobile\tout=$OUT"
|
||||
;;
|
||||
mobile-verify|verify)
|
||||
"$0" test
|
||||
test -f "$APP_DIR/target/release/workout-app" || {{ echo 'missing target/release/workout-app; run mobile-release first' >&2; exit 1; }}
|
||||
case "$ORIGIN" in https://*) ;; *) echo 'HEMX_WORKOUT_ORIGIN must be HTTPS' >&2; exit 1;; esac
|
||||
for f in release-manifest.json asset-integrity.tsv android/twa-release.json ios/webview-release.json BLOCKERS.md; do test -f "$OUT/$f" || {{ echo "missing $OUT/$f" >&2; exit 1; }}; done
|
||||
grep -F "$RUNTIME_PATH" "$OUT/release-manifest.json" >/dev/null
|
||||
grep -F "$RUNTIME_SHA" "$OUT/asset-integrity.tsv" >/dev/null
|
||||
grep -F 'app-owned command/event/projection records' "$OUT/release-manifest.json" >/dev/null
|
||||
grep -F 'host_result_kinds' "$OUT/android/twa-release.json" >/dev/null
|
||||
grep -F 'host_result_kinds' "$OUT/ios/webview-release.json" >/dev/null
|
||||
echo "workout-mobile-verified\tout=$OUT"
|
||||
;;
|
||||
doctor)
|
||||
write_blockers
|
||||
cat "$OUT/BLOCKERS.md"
|
||||
;;
|
||||
*)
|
||||
echo 'usage: ./hemx-workout dev|test|build|mobile-release|mobile-verify|doctor' >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
"#,
|
||||
runtime_path = hemx_js::RUNTIME_JS_PATH,
|
||||
runtime_sha = hemx_js::RUNTIME_JS_HASH,
|
||||
)
|
||||
}
|
||||
|
||||
fn copy_dir(source: &Path, destination: &Path) -> std::io::Result<()> {
|
||||
fs::create_dir_all(destination)?;
|
||||
for entry in fs::read_dir(source)? {
|
||||
@@ -958,6 +1072,11 @@ mod tests {
|
||||
let main_rs = fs::read_to_string(destination.join("src/main.rs")).expect("main rs");
|
||||
assert!(main_rs.contains("hemx_workout_app"));
|
||||
assert!(!main_rs.contains("hemx_workout_example"));
|
||||
let command = fs::read_to_string(destination.join("hemx-workout")).expect("command");
|
||||
assert!(command.contains("mobile-release"));
|
||||
assert!(command.contains("mobile-verify"));
|
||||
assert!(command.contains(hemx_js::RUNTIME_JS_HASH));
|
||||
assert!(command.contains("app-owned command/event/projection records"));
|
||||
assert!(destination.join("src/lib.rs").exists());
|
||||
assert!(destination.join("templates/workout.heml").exists());
|
||||
let _ = fs::remove_dir_all(&destination);
|
||||
|
||||
Reference in New Issue
Block a user