appmachine 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ <p align="center" style="margin-top: 40px;">
2
+ <img src="https://raw.githubusercontent.com/max-matinpalo/appmachine/refs/heads/main/template/icon.png" alt="Project logo" width="160">
3
+ </p>
4
+ <br>
5
+
6
+
7
+ # AppMachine
8
+
9
+ **‼️ ATTENTON ‼️:** This package is not ready for mainstream use.
10
+ It’s meant as inspiration for advanced developers who want to automate their own
11
+ build pipelines.
12
+
13
+ **Personal motivation:** a fully automatic build flow for iOS + Android 🤖🧰
14
+ Configure once, one click trigger, download ready signed apps.
15
+ Never again waste time playing with Xcode, AndroidStudio and Capacitor.
16
+
17
+
18
+ ## Introduction
19
+
20
+ AppMachine builds **signed native iOS and Android apps** for web apps.
21
+ It’s built on **Capacitor + GitHub Actions**. You trigger builds by pushing tags.
22
+ You don’t need to install any build tools on your own machine.
23
+
24
+ ## Install
25
+ Inside your normal React project:
26
+
27
+ ```bash
28
+ npm install appmachine
29
+ npx appmachine
30
+ ```
31
+ Configure build variabels and secrets on github
32
+
33
+ ## Example usage
34
+ 1. git tag android; git push origin android
35
+ 2. github server builds app
36
+ 4. download ready app
37
+
38
+ Builds are triggered by setting tags (ios, iosdev, android, androiddev).
39
+ Because typically we only need to generate new ios/android apps rarely,
40
+ only when native features change.
41
+
42
+
43
+ ## Base Config
44
+ **Check out docs folder for detailed instruction for ios / android configs**
45
+
46
+ | Type | Name | Description | Example |
47
+ |---|---|---|---|
48
+ | Variable | `APP_SERVER_URL` | Base URL where the app loads from | `https://app.example.com` |
49
+ | Variable | `APP_ID` | iOS Bundle ID (App ID) | `com.company.app` |
50
+ | Variable | `APP_NAME` | Visible app name | `ExampleApp` |
51
+
52
+
53
+ ## Android Configs
54
+ | Type | Name | Description | Example |
55
+ |---|---|---|---|
56
+ | Secret | `ANDROID_KEYSTORE` | Base64 keystore from the keystore workflow | `BASE64...` |
57
+ | Secret | `ANDROID_KEYSTORE_PASSWORD` | Keystore password used to sign the app | `password123` |
58
+
59
+ ## iOS Configs
60
+ | Type | Name | Description | Example |
61
+ |---|---|---|---|
62
+ | Variable | `IOS_TEAM_ID` | Apple Developer Team ID | `A1B2C3D4E5` |
63
+ | Secret | `IOS_CERT_BASE64` | Base64 of your **Distribution** `.p12` | `MII...` |
64
+ | Secret | `IOS_CERT_PASSWORD` | Password for the `.p12` | `your-password` |
65
+ | Secret | `IOS_PROFILE_BASE64` | Base64 of your `.mobileprovision` | `MII...` |
66
+
67
+
68
+ ---
69
+
70
+
71
+
72
+ ## How it works overview
73
+ Trigger github action via tags (`iosdev`, `ios`, `androiddev` or`android`).
74
+
75
+ The development and production builds only differs in used variabels and secrets.
76
+ Tag `iosdev` - use the variables of environment `Development`.
77
+ Tag `ios` - use variables of environment `Production`.
78
+
79
+ 1. **Capacitor setup** (same for iOS and Android)
80
+ - Installs Capacitor tooling
81
+ - Writes `capacitor.config.json` from GitHub Variables
82
+ - Generates native projects + assets with Capacitor
83
+ - Then runs iOS or Android specific signing
84
+ 2. **Setup signing**
85
+ 3. **Build**
86
+
87
+ The easy step 1 is same for ios and android.
88
+
89
+ At the moment, the build pipeline does **not** build your web app. Instead, it
90
+ fetches the web app from a server. This means you must have the app deployed
91
+ somewhere (for example Vercel).
92
+
93
+ The reason: the “genius” way to ship apps is a **minimal native shell** with
94
+ **OTA updates** and **offline support** via a service worker 😃
95
+
96
+ If we want to make this package easier for junior developers, we should add an
97
+ optional step to **build the web app during the pipeline** and ship the `dist`
98
+ bundle inside the native app. This is very easy to add, and could be controlled
99
+ with a workflow option/flag 😃
100
+
101
+
102
+
103
+ ## Native features
104
+
105
+ In general, when we want code that runs on all platforms, we should prefer
106
+ **Web APIs** whenever possible (camera, files, etc.). If a solid Web API exists,
107
+ use it before reaching for native features.
108
+
109
+ Not everything is available (or reliable) via Web APIs. For those cases, we use
110
+ **Capacitor plugins**.
111
+
112
+ This workflow installs the latest Capacitor packages on the build machine
113
+ (`@capacitor/core`, `@capacitor/ios`, `@capacitor/android`, `@capacitor/cli`),
114
+ so you don’t need to install them in your app.
115
+
116
+ Example: if you want haptics, just install the plugin in your React project:
117
+
118
+ ```bash
119
+ npm install @capacitor/haptics
120
+ ```
121
+
package/bin/install.js ADDED
@@ -0,0 +1,52 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+
5
+ function die(msg) {
6
+ console.error("❌ " + msg);
7
+ process.exit(1);
8
+ }
9
+
10
+
11
+ // Recursively copies files or directories from source (s) to destination (d)
12
+ function copy(s, d) {
13
+ const stat = fs.statSync(s);
14
+
15
+ // If it's a folder, ensure destination exists and copy all children
16
+ if (stat.isDirectory()) {
17
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
18
+ fs.readdirSync(s).forEach(f => copy(path.join(s, f), path.join(d, f)));
19
+ return;
20
+ }
21
+
22
+ // Otherwise, just copy the file
23
+ fs.copyFileSync(s, d);
24
+ }
25
+
26
+
27
+ // Main setup: moves GitHub workflows and AppMachine templates into the project root
28
+ function install() {
29
+ const root = process.cwd();
30
+ const src = path.join(__dirname, '../src');
31
+ const dest = path.join(root, '.github/workflows');
32
+ const temp = path.join(__dirname, '../template');
33
+ const mach = path.join(root, 'appmachine');
34
+
35
+ // 1. Copy all .yml workflow files to .github/workflows
36
+ if (fs.existsSync(src)) {
37
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
38
+ fs.readdirSync(src).forEach(f => {
39
+ if (f.endsWith('.yml')) fs.copyFileSync(path.join(src, f), path.join(dest, f));
40
+ });
41
+ }
42
+
43
+ // 2. Recursively copy the template folder to /appmachine
44
+ if (fs.existsSync(temp)) copy(temp, mach);
45
+ }
46
+
47
+
48
+ try {
49
+ install();
50
+ } catch (err) {
51
+ die(err.message || "Install failed");
52
+ }
@@ -0,0 +1,37 @@
1
+ # AppMachine Android Build Workflow
2
+
3
+ Build pipeline that turns your webapp into a **signed Android app**.
4
+
5
+
6
+ ## How it works
7
+ 1. Start a build by pushing tag `android` or `androiddev` on the branch you want to build.
8
+ 2. After workflow completes with success, download the app from workflow artifacts.
9
+ 3. Install the `.apk` to your Android phone or deploy the `.aab` to Google Play.
10
+
11
+ The development and production builds only differ in used variables and secrets.
12
+ Tag `androiddev` - use the variables of environment `Development`.
13
+ Tag `android` - use variables of environment `Production`.
14
+
15
+ The workflow will output the app in two formats: `.apk` and `.aab`
16
+ The `.apk` format is for direct install/testing on devices.
17
+ The `.aab` format is for upload to Google Play.
18
+
19
+
20
+
21
+ ## Setup
22
+ 1. Run ONCE the generate keystore workflow.
23
+ Only ONCE, because the keystore must stay equal when you deploy updates to your app.
24
+ 2. Store the keystore workflow result somewhere safe and add it as GitHub Environment
25
+ secrets: `ANDROID_KEYSTORE` (Base64) and `ANDROID_KEYSTORE_PASSWORD`.
26
+
27
+ ---
28
+
29
+ ## Required configuration (GitHub repo)
30
+
31
+ | Type | Name | Description | Example |
32
+ |---|---|---|---|
33
+ | Variable | `APP_SERVER_URL` | Base URL where the app loads from | `https://app.example.com` |
34
+ | Variable | `APP_ID` | Android App ID (package name) | `com.company.app` |
35
+ | Variable | `APP_NAME` | Visible app name | `TeamFeedback` |
36
+ | Secret | `ANDROID_KEYSTORE` | Base64 keystore from the keystore workflow | `BASE64...` |
37
+ | Secret | `ANDROID_KEYSTORE_PASSWORD` | Keystore password used to sign the app | `password123` |
package/docs/ios.md ADDED
@@ -0,0 +1,112 @@
1
+ # AppMachine iOS Build Workflow
2
+
3
+ iOS build pipeline that turns your webapp into a **signed iOS `.ipa`** on **GitHub Actions**. No local Xcode needed.
4
+
5
+
6
+ ## How it works
7
+
8
+ 1. Setup Github Environemnt **Variables** + **Secrets**.
9
+ 2. Start a build by pushing tag `iosdev` or `ios`on the branch you want to build.
10
+ 3. After workflow completes with success, download app from workflow artifacts.
11
+ 4. Easiest way to upload app to apple is to use apple's **Transporter** app.
12
+ Download it from appstore, just drag & drop your .ipa file there and click upload.
13
+
14
+ The development and production builds only differs in used variabels and secrets.
15
+ Tag `iosdev` - use the variables of environment `Development`.
16
+ Tag `ios` - use variables of environment `Production`.
17
+
18
+ ---
19
+
20
+ ## Required configuration (GitHub repo)
21
+
22
+ | Type | Name | Description | Example |
23
+ |---|---|---|---|
24
+ | Variable | `APP_SERVER_URL` | Base URL where the app loads from | `https://app.example.com` |
25
+ | Variable | `APP_ID` | iOS Bundle ID (App ID) | `com.company.app` |
26
+ | Variable | `APP_NAME` | Visible app name | `TeamFeedback` |
27
+ | Variable | `IOS_TEAM_ID` | Apple Developer Team ID | `A1B2C3D4E5` |
28
+ | Secret | `IOS_CERT_BASE64` | Base64 of your **Distribution** `.p12` | `MII...` |
29
+ | Secret | `IOS_CERT_PASSWORD` | Password for the `.p12` | `your-password` |
30
+ | Secret | `IOS_PROFILE_BASE64` | Base64 of your `.mobileprovision` | `MII...` |
31
+
32
+ ---
33
+
34
+ ## Apple setup
35
+
36
+ ### 1) Create Bundle ID
37
+ Create the bundle identifier you’ll ship as.
38
+
39
+ - Apple Developer → **Certificates, Identifiers & Profiles**
40
+ - **Identifiers** → `+` → **App IDs**
41
+ - Set on github env `APP_ID` (example: `com.company.app`)
42
+
43
+ ### 2) Create App
44
+ Create the app container in App Store Connect.
45
+
46
+ - App Store Connect → **My Apps** → `+` → **New App**
47
+ - Pick the same **Bundle ID** you created above
48
+
49
+ ### 3) Set IOS_TEAM_ID
50
+ - Find your team id under Apple Developer → **Membership**
51
+ - Copy it as repo variable `IOS_TEAM_ID` to github
52
+
53
+
54
+ ### 4) Distribution certificate
55
+ If you don't have one yet, at the end of this document you find instruction how to make. Yes, stupid apple flow. Happily just once per year.
56
+
57
+ Set github secret: `IOS_CERT_BASE64`
58
+ By copying your certificate in base64 with
59
+ base64 -i distribution.p12 | pbcopy
60
+
61
+
62
+ ### 5) Provisioning profile → `.mobileprovision`
63
+ This ties together: **App ID + Distribution certificate**.
64
+
65
+ - Apple Developer → Profiles → +
66
+ - Choose **App Store** (distribution)
67
+ - Select your **App ID**
68
+ - Select your **Apple Distribution** certificate
69
+ - Download the `.mobileprovision`
70
+
71
+ **Convert to base64 (macOS) and copy to clipboard:**
72
+ base64 -i Profile.mobileprovision | pbcopy
73
+
74
+
75
+
76
+ ## Apple Distribution certificate generation
77
+
78
+ #### 1. Create the CSR on your Mac (Keychain Access) FIRST
79
+ 1. Open **Keychain Access**
80
+ 2. Menu: **Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority…**
81
+ 3. Fill:
82
+ - **User Email Address**: your Apple ID email
83
+ - **Common Name**: e.g. `TeamFeedback Distribution`
84
+ - **CA Email Address**: leave empty
85
+ 4. Select:
86
+ - ✅ **Saved to disk**
87
+ - ✅ **Let me specify key pair information**
88
+ 5. Continue:
89
+ - Key size: **2048 bits**
90
+ - Algorithm: **RSA**
91
+ 6. Save the `.certSigningRequest` (CSR) file
92
+
93
+ #### 2. Create the Distribution certificate in Apple Developer (upload the CSR)
94
+ 1. Apple Developer → **Certificates, Identifiers & Profiles**
95
+ 2. **Certificates** → `+`
96
+ 3. Choose **Apple Distribution**
97
+ 4. Upload the CSR you created above
98
+ 5. Download the generated certificate (`.cer`)
99
+
100
+ #### 3. Install the `.cer` into Keychain
101
+ 1. Double-click the downloaded `.cer`
102
+ 2. In **Keychain Access**, find **Apple Distribution: ...**
103
+ 3. Expand it and confirm it has a **private key** under it
104
+
105
+ #### 4. Export as `.p12`
106
+ 1. Right-click **Apple Distribution: ...** (the item that includes the private key)
107
+ 2. **Export…** → choose **.p12**
108
+ 3. Set an export password
109
+ 4. Save as `distribution.p12`
110
+
111
+ #### 5. Convert `.p12` to base64 (macOS) and copy to clipboard
112
+ base64 -i distribution.p12 | pbcopy
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "appmachine",
3
+ "version": "0.0.2",
4
+ "description": "Native iOS/Android shell generator via GitHub Actions",
5
+ "author": "Max Matinpalo",
6
+ "type": "module",
7
+ "scripts": {
8
+ "install": "node ./scripts/install.js"
9
+ }
10
+ }
@@ -0,0 +1,125 @@
1
+ name: AppMachine / Android
2
+ on:
3
+ push:
4
+ tags: [androiddev, android]
5
+
6
+ permissions:
7
+ contents: write
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-22.04
12
+ environment: ${{ github.ref == 'refs/tags/androiddev' && 'Development' || 'Production' }}
13
+ env:
14
+ APP_SERVER_URL: ${{ vars.APP_SERVER_URL }}
15
+ APP_ID: ${{ vars.APP_ID }}
16
+ APP_NAME: ${{ vars.APP_NAME }}
17
+ ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
18
+ ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: actions/setup-node@v4
24
+ with: { node-version: lts/*, cache: npm }
25
+
26
+ - uses: actions/setup-java@v4
27
+ with: { distribution: temurin, java-version: "21" }
28
+
29
+ - uses: android-actions/setup-android@v3
30
+
31
+ - name: 🔎 Preflight Checks
32
+ run: |
33
+ set -euo pipefail
34
+ req() { [ -n "${!1:-}" ] || { echo "❌ Missing: $1" >&2; exit 1; }; }
35
+ req APP_SERVER_URL; req APP_ID
36
+ req ANDROID_KEYSTORE_BASE64; req ANDROID_KEYSTORE_PASSWORD
37
+ echo "✅ All required variables and secrets are present."
38
+
39
+ - name: 📦 Install Dependencies
40
+ run: |
41
+ set -euo pipefail
42
+ [ -f package-lock.json ] && npm ci || npm install
43
+ npm i -D @capacitor/core @capacitor/cli @capacitor/android @capacitor/assets \
44
+ --no-save --no-package-lock --no-fund --no-audit
45
+
46
+ - name: ⚙️ Generate Config & WWW
47
+ run: |
48
+ set -euo pipefail
49
+ rm -rf www && mkdir -p www && cp -R appmachine/www/. www/
50
+ rm -f capacitor.config.*
51
+
52
+ node -e '
53
+ const fs = require("fs"), { URL } = require("url");
54
+ const raw = process.env.APP_SERVER_URL;
55
+ try { new URL(raw); } catch { console.error("❌ Invalid URL"); process.exit(1); }
56
+
57
+ const u = new URL(raw.replace(/\/$/, "")), host = u.hostname;
58
+ const cfg = {
59
+ appId: process.env.APP_ID,
60
+ appName: process.env.APP_NAME || "App",
61
+ webDir: "www",
62
+ server: { url: u.href, cleartext: true, allowNavigation: [host, "*." + host] }
63
+ };
64
+ fs.writeFileSync("capacitor.config.json", JSON.stringify(cfg, null, 2));
65
+ '
66
+
67
+ - name: 📱 Capacitor Sync & Assets
68
+ run: |
69
+ set -euo pipefail
70
+ rm -rf android && npx --no-install cap add android
71
+ mkdir -p assets
72
+ if [ -f "appmachine/icon.png" ]; then
73
+ echo "✅ Custom icon found"
74
+ for f in logo logo-dark splash splash-dark; do cp "appmachine/icon.png" "assets/$f.png"; done
75
+ npx --no-install capacitor-assets generate --android --assetPath assets
76
+ fi
77
+ npx --no-install cap sync android
78
+
79
+ - name: 🔐 Configure Signing
80
+ run: |
81
+ set -euo pipefail
82
+ printf '%s' "$ANDROID_KEYSTORE_BASE64" | tr -d '\n\r' | base64 -d > android/keystore.jks
83
+
84
+ cat > android/app/signing.gradle <<EOF
85
+ android {
86
+ signingConfigs {
87
+ release {
88
+ storeFile file("../keystore.jks")
89
+ storePassword System.getenv("ANDROID_KEYSTORE_PASSWORD")
90
+ keyAlias "release"
91
+ keyPassword System.getenv("ANDROID_KEYSTORE_PASSWORD")
92
+ }
93
+ }
94
+ buildTypes { release { signingConfig signingConfigs.release } }
95
+ }
96
+ EOF
97
+
98
+ node -e '
99
+ const fs = require("fs"), p = "android/app/build.gradle";
100
+ if (!fs.existsSync(p)) process.exit(1);
101
+ const s = fs.readFileSync(p, "utf8");
102
+ if (!s.includes("signing.gradle")) fs.appendFileSync(p, "\napply from: \"signing.gradle\"\n");
103
+ '
104
+
105
+ - name: 🏗️ Gradle Build
106
+ run: |
107
+ set -euo pipefail
108
+ chmod +x android/gradlew
109
+ (cd android && ./gradlew --no-daemon :app:bundleRelease :app:assembleRelease)
110
+
111
+ - name: 📂 Flatten Artifacts
112
+ run: |
113
+ mkdir -p dist
114
+ # Find files recursively and copy them to the flat 'dist' folder
115
+ find android/app/build/outputs -name "*.apk" -exec cp {} dist/ \;
116
+ find android/app/build/outputs -name "*.aab" -exec cp {} dist/ \;
117
+
118
+ echo "✅ Contents of artifacts folder:"
119
+ ls -l dist/
120
+
121
+ - name: 📤 Upload Artifacts
122
+ uses: actions/upload-artifact@v4
123
+ with:
124
+ name: android-release
125
+ path: dist/*
@@ -0,0 +1,113 @@
1
+ name: Generate Android Keystore
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ target:
7
+ description: "dev or prod"
8
+ required: true
9
+ type: choice
10
+ options: [dev, prod]
11
+ cn:
12
+ description: "Common Name"
13
+ required: true
14
+
15
+ jobs:
16
+ preflight:
17
+ runs-on: ubuntu-latest
18
+ environment: ${{ inputs.target == 'prod' && 'Production' || 'Development' }}
19
+ steps:
20
+ - name: 🛑 Safety Checks
21
+ shell: bash
22
+ env:
23
+ PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
24
+ EXISTING_KEY: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
25
+ run: |
26
+ set -euo pipefail
27
+
28
+ if [ -z "$PASS" ]; then
29
+ echo "❌ Error: 'ANDROID_KEYSTORE_PASSWORD' secret is missing in this environment!"
30
+ exit 1
31
+ fi
32
+
33
+ if [ -n "$EXISTING_KEY" ]; then
34
+ echo "❌ Error: 'ANDROID_KEYSTORE_BASE64' secret is ALREADY set!"
35
+ echo " -------------------------------------------------------------"
36
+ echo " ⚠️ CRITICAL: You must use the same key to deploy updates."
37
+ exit 1
38
+ fi
39
+ echo "✅ Preflight passed."
40
+
41
+ gen:
42
+ needs: preflight
43
+ runs-on: ubuntu-latest
44
+ environment: ${{ inputs.target == 'prod' && 'Production' || 'Development' }}
45
+
46
+ steps:
47
+ - uses: actions/setup-java@v4
48
+ with:
49
+ distribution: temurin
50
+ java-version: "17"
51
+
52
+ - name: Generate base64
53
+ shell: bash
54
+ env:
55
+ PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
56
+ run: |
57
+ set -euo pipefail
58
+
59
+ # Safety check
60
+ if [ -z "$PASS" ]; then echo "❌ Secret missing!"; exit 1; fi
61
+
62
+ TARGET='${{ inputs.target }}'
63
+ DNAME="CN=${{ inputs.cn }}"
64
+ ALIAS='release'
65
+ JKS="release-${TARGET}.jks"
66
+ OUT="keystore-${TARGET}.base64.txt"
67
+
68
+ # 1. Generate Key
69
+ keytool -genkeypair -v \
70
+ -keystore "$JKS" \
71
+ -storetype JKS \
72
+ -alias "$ALIAS" \
73
+ -keyalg RSA \
74
+ -keysize 2048 \
75
+ -validity 10000 \
76
+ -storepass "$PASS" \
77
+ -keypass "$PASS" \
78
+ -dname "$DNAME"
79
+
80
+ # 2. Encode
81
+ base64 -w0 "$JKS" > "$OUT"
82
+ echo >> "$OUT"
83
+
84
+ # 3. Security: Delete raw binary key immediately
85
+ rm -f "$JKS"
86
+
87
+ - name: Upload artifact
88
+ uses: actions/upload-artifact@v4
89
+ with:
90
+ name: android-keystore-${{ inputs.target }}
91
+ path: keystore-${{ inputs.target }}.base64.txt
92
+ retention-days: 1
93
+
94
+ - name: 📝 Next steps
95
+ shell: bash
96
+ run: |
97
+ {
98
+ echo "## ✅ Next steps"
99
+ echo "1. Download the artifact **android-keystore-${{ inputs.target }}**."
100
+ echo "2. Copy the Base64 string."
101
+ echo "3. Save it as **ANDROID_KEYSTORE_BASE64** in the **${{ inputs.target == 'prod' && 'Production' || 'Development' }}** environment secrets."
102
+ echo ""
103
+ echo "## ⚠️ Backup"
104
+ echo "- Store the Base64 somewhere safe."
105
+ echo "- Artifact expires in **24h**."
106
+ } >> "$GITHUB_STEP_SUMMARY"
107
+
108
+ - name: Cleanup Workspace
109
+ if: always()
110
+ shell: bash
111
+ run: |
112
+ set -euo pipefail
113
+ rm -f keystore-*.base64.txt