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 +121 -0
- package/bin/install.js +52 -0
- package/docs/android.md +37 -0
- package/docs/ios.md +112 -0
- package/package.json +10 -0
- package/src/android.yml +125 -0
- package/src/android_keygen.yml +113 -0
- package/src/ios.yml +183 -0
- package/template/icon.png +0 -0
- package/template/setup_guide.txt +1 -0
- package/template/www/index.html +544 -0
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
|
+
}
|
package/docs/android.md
ADDED
|
@@ -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
package/src/android.yml
ADDED
|
@@ -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
|