@upfluxhq/cli 1.0.1 → 1.1.0
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 +62 -1
- package/dist/commands/release.js +91 -11
- package/dist/config/configStore.d.ts +14 -0
- package/dist/config/configStore.js +26 -0
- package/dist/utils/native-detector.d.ts +48 -0
- package/dist/utils/native-detector.js +198 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,6 +100,8 @@ upflux release [options]
|
|
|
100
100
|
| `-m, --mandatory` | | Mark update as mandatory |
|
|
101
101
|
| `-r, --rollout <number>` | | Rollout percentage (0-100, default: 100) |
|
|
102
102
|
| `-v, --version-range <range>` | | Binary version range (e.g., >=1.0.0) |
|
|
103
|
+
| `--min-binary-version <version>` | | Minimum native binary version required. Devices below this are directed to update from the App Store / Play Store before receiving this OTA. |
|
|
104
|
+
| `--skip-native-check` | | Skip automatic native change detection (useful in CI/CD) |
|
|
103
105
|
| `--dev` | | Create development bundle |
|
|
104
106
|
| `--skip-bundle` | | Skip bundling, use existing files in output dir |
|
|
105
107
|
| `--schedule` | | Create as draft, set schedule time in dashboard |
|
|
@@ -168,6 +170,63 @@ upflux release --label v1.0.0 --capacitor --web-dir dist
|
|
|
168
170
|
|
|
169
171
|
---
|
|
170
172
|
|
|
173
|
+
## Native Change Detection
|
|
174
|
+
|
|
175
|
+
The CLI automatically detects native code changes since your last release and warns you when a store update may be required. This helps prevent devices from receiving OTA updates that depend on native features not yet available in their installed binary.
|
|
176
|
+
|
|
177
|
+
### How It Works
|
|
178
|
+
|
|
179
|
+
1. After each successful release, the CLI saves the current **git commit hash** to `.upflux`.
|
|
180
|
+
2. On the next release, it runs `git diff <lastHash>..HEAD` against native files.
|
|
181
|
+
3. If changes are found, it warns you and offers to auto-set `--min-binary-version`.
|
|
182
|
+
|
|
183
|
+
> **Note:** Detection requires git. If the project is not a git repo, detection is silently skipped.
|
|
184
|
+
|
|
185
|
+
### Files Watched
|
|
186
|
+
|
|
187
|
+
| File | What is detected |
|
|
188
|
+
|---|---|
|
|
189
|
+
| `android/app/build.gradle` | `versionName` bump, `minSdkVersion` change |
|
|
190
|
+
| `android/app/src/main/AndroidManifest.xml` | New permissions |
|
|
191
|
+
| `ios/Podfile` | New native pods |
|
|
192
|
+
| `ios/Podfile.lock` | Any pod version change |
|
|
193
|
+
| `package.json` | New native dependency packages |
|
|
194
|
+
|
|
195
|
+
### Example Interaction
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
⚠️ Native changes detected since last release:
|
|
199
|
+
|
|
200
|
+
Android:
|
|
201
|
+
• build.gradle: versionName → 2.0.0
|
|
202
|
+
• AndroidManifest.xml: New permission: android.permission.CAMERA
|
|
203
|
+
|
|
204
|
+
💡 Suggested flags:
|
|
205
|
+
--min-binary-version 2.0.0
|
|
206
|
+
--version-range ">=2.0.0"
|
|
207
|
+
|
|
208
|
+
? How would you like to handle these native changes?
|
|
209
|
+
❯ Auto-apply suggested flags (--min-binary-version 2.0.0 --version-range ">=2.0.0")
|
|
210
|
+
Enter min-binary-version manually
|
|
211
|
+
Continue without (not recommended for native changes)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Setting `--min-binary-version` Manually
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Require devices to have at least v2.0.0 binary before receiving this OTA
|
|
218
|
+
upflux release --label v2.0.0-ota --min-binary-version 2.0.0 --version-range ">=2.0.0"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Skipping in CI/CD
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# In CI, skip interactive detection and pass flag explicitly
|
|
225
|
+
upflux release --label v2.0.0 --min-binary-version 2.0.0 --skip-native-check
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
171
230
|
### `status`
|
|
172
231
|
|
|
173
232
|
Check deployment status and recent releases.
|
|
@@ -290,7 +349,9 @@ The CLI stores credentials in `.upflux` in the current directory:
|
|
|
290
349
|
"deploymentName": "staging"
|
|
291
350
|
}
|
|
292
351
|
],
|
|
293
|
-
"activeIndex": 0
|
|
352
|
+
"activeIndex": 0,
|
|
353
|
+
"lastReleaseHash": "a3f8c12d...",
|
|
354
|
+
"lastReleaseLabel": "v1.5.0"
|
|
294
355
|
}
|
|
295
356
|
```
|
|
296
357
|
|
package/dist/commands/release.js
CHANGED
|
@@ -45,6 +45,7 @@ const inquirer_1 = __importDefault(require("inquirer"));
|
|
|
45
45
|
const archiver_1 = __importDefault(require("archiver"));
|
|
46
46
|
const apiClient_1 = require("../client/apiClient");
|
|
47
47
|
const configStore_1 = require("../config/configStore");
|
|
48
|
+
const native_detector_1 = require("../utils/native-detector");
|
|
48
49
|
/**
|
|
49
50
|
* Default output directory for React Native bundle
|
|
50
51
|
*/
|
|
@@ -325,9 +326,11 @@ const release = new commander_1.Command("release")
|
|
|
325
326
|
.option("-m, --mandatory", "Mark update as mandatory", false)
|
|
326
327
|
.option("-r, --rollout <number>", "Rollout percentage (0-100)", "100")
|
|
327
328
|
.option("-v, --version-range <range>", "Binary version range (e.g., >=1.0.0)")
|
|
329
|
+
.option("--min-binary-version <version>", "Minimum native binary version devices must have before receiving this OTA update. Devices below this version will be directed to update from the app store.")
|
|
328
330
|
.option("--dev", "Create development bundle", false)
|
|
329
331
|
.option("--schedule", "Create as draft, set schedule time in dashboard")
|
|
330
332
|
.option("--skip-bundle", "Skip bundling, use existing files in output dir", false)
|
|
333
|
+
.option("--skip-native-check", "Skip automatic native change detection (useful in CI/CD)", false)
|
|
331
334
|
.option("--capacitor", "Force Capacitor mode (auto-detected if capacitor.config exists)")
|
|
332
335
|
.option("--web-dir <path>", "Web directory to bundle for Capacitor (default: from config or www)")
|
|
333
336
|
.action(async (opts) => {
|
|
@@ -619,6 +622,80 @@ const release = new commander_1.Command("release")
|
|
|
619
622
|
}
|
|
620
623
|
}
|
|
621
624
|
// ========================================
|
|
625
|
+
// NATIVE CHANGE DETECTION PHASE
|
|
626
|
+
// ========================================
|
|
627
|
+
if (!opts.skipNativeCheck) {
|
|
628
|
+
const gitStatus = (0, native_detector_1.getGitRepoStatus)();
|
|
629
|
+
if (!gitStatus) {
|
|
630
|
+
// Not a git repo or git not installed — skip silently
|
|
631
|
+
console.log(chalk_1.default.gray(" ⓘ Git not detected — skipping native change check"));
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
const lastHash = configStore_1.configStore.getLastReleaseHash();
|
|
635
|
+
const lastLabel = configStore_1.configStore.getLastReleaseLabel();
|
|
636
|
+
if (!lastHash) {
|
|
637
|
+
// First release — nothing to compare against
|
|
638
|
+
console.log(chalk_1.default.gray(" ⓘ First release — native change detection will run from next release"));
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
console.log(chalk_1.default.gray(` 🔍 Checking native changes since ${lastLabel || lastHash.slice(0, 7)}...`));
|
|
642
|
+
const detection = (0, native_detector_1.detectNativeChanges)(lastHash);
|
|
643
|
+
if (detection.hasNativeChanges) {
|
|
644
|
+
(0, native_detector_1.displayNativeChangeWarning)(detection);
|
|
645
|
+
// Only prompt if --min-binary-version not already provided
|
|
646
|
+
if (!opts.minBinaryVersion) {
|
|
647
|
+
const choices = [
|
|
648
|
+
...(detection.suggestedMinBinaryVersion
|
|
649
|
+
? [{
|
|
650
|
+
name: `Auto-apply suggested flags (--min-binary-version ${detection.suggestedMinBinaryVersion} --version-range "${detection.suggestedVersionRange}")`,
|
|
651
|
+
value: 'auto',
|
|
652
|
+
}]
|
|
653
|
+
: []),
|
|
654
|
+
{ name: 'Enter min-binary-version manually', value: 'custom' },
|
|
655
|
+
{ name: 'Continue without (not recommended for native changes)', value: 'skip' },
|
|
656
|
+
];
|
|
657
|
+
const { action } = await inquirer_1.default.prompt([{
|
|
658
|
+
type: 'list',
|
|
659
|
+
name: 'action',
|
|
660
|
+
message: 'How would you like to handle these native changes?',
|
|
661
|
+
choices,
|
|
662
|
+
}]);
|
|
663
|
+
if (action === 'auto') {
|
|
664
|
+
opts.minBinaryVersion = detection.suggestedMinBinaryVersion;
|
|
665
|
+
if (!opts.versionRange && detection.suggestedVersionRange) {
|
|
666
|
+
opts.versionRange = detection.suggestedVersionRange;
|
|
667
|
+
}
|
|
668
|
+
console.log(chalk_1.default.green(` ✓ --min-binary-version ${opts.minBinaryVersion}`));
|
|
669
|
+
console.log(chalk_1.default.green(` ✓ --version-range ${opts.versionRange}`));
|
|
670
|
+
}
|
|
671
|
+
else if (action === 'custom') {
|
|
672
|
+
const { version } = await inquirer_1.default.prompt([{
|
|
673
|
+
type: 'input',
|
|
674
|
+
name: 'version',
|
|
675
|
+
message: 'Enter minimum binary version (e.g., 2.0.0):',
|
|
676
|
+
default: detection.suggestedMinBinaryVersion,
|
|
677
|
+
validate: (v) => /^\d+\.\d+\.\d+/.test(v) || 'Enter a valid semver version (e.g., 2.0.0)',
|
|
678
|
+
}]);
|
|
679
|
+
opts.minBinaryVersion = version;
|
|
680
|
+
if (!opts.versionRange) {
|
|
681
|
+
opts.versionRange = `>=${version}`;
|
|
682
|
+
}
|
|
683
|
+
console.log(chalk_1.default.green(` ✓ --min-binary-version ${opts.minBinaryVersion}`));
|
|
684
|
+
console.log(chalk_1.default.green(` ✓ --version-range ${opts.versionRange}`));
|
|
685
|
+
}
|
|
686
|
+
// 'skip' — continue without setting flags
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
console.log(chalk_1.default.gray(` ⓘ --min-binary-version already set to ${opts.minBinaryVersion}`));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
console.log(chalk_1.default.green(' ✓ No native changes detected'));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// ========================================
|
|
622
699
|
// UPLOAD PHASE
|
|
623
700
|
// ========================================
|
|
624
701
|
console.log(chalk_1.default.blue("\n⬆ Uploading release..."));
|
|
@@ -637,6 +714,9 @@ const release = new commander_1.Command("release")
|
|
|
637
714
|
if (opts.versionRange) {
|
|
638
715
|
console.log(` Version Range: ${opts.versionRange}`);
|
|
639
716
|
}
|
|
717
|
+
if (opts.minBinaryVersion) {
|
|
718
|
+
console.log(` Min Binary Version: ${chalk_1.default.cyan(opts.minBinaryVersion)}`);
|
|
719
|
+
}
|
|
640
720
|
// ========================================
|
|
641
721
|
// PRESIGNED URL UPLOAD FLOW
|
|
642
722
|
// ========================================
|
|
@@ -701,18 +781,9 @@ const release = new commander_1.Command("release")
|
|
|
701
781
|
}
|
|
702
782
|
}
|
|
703
783
|
// Step 3: Complete upload and create release record
|
|
704
|
-
const completeResponse = await apiClient_1.apiClient.post("/cli/releases/upload/complete", {
|
|
705
|
-
uploadId: initResponse.data.uploadId,
|
|
706
|
-
releaseLabel: opts.label,
|
|
707
|
-
mandatory: opts.mandatory,
|
|
708
|
-
rolloutPercentage: rolloutValidation.value,
|
|
709
|
-
binaryVersionRange: opts.versionRange,
|
|
710
|
-
platform,
|
|
711
|
-
isDraft: opts.schedule,
|
|
784
|
+
const completeResponse = await apiClient_1.apiClient.post("/cli/releases/upload/complete", Object.assign(Object.assign({ uploadId: initResponse.data.uploadId, releaseLabel: opts.label, mandatory: opts.mandatory, rolloutPercentage: rolloutValidation.value, binaryVersionRange: opts.versionRange }, (opts.minBinaryVersion ? { minBinaryVersion: opts.minBinaryVersion } : {})), { platform, isDraft: opts.schedule,
|
|
712
785
|
// Include bundle sizes
|
|
713
|
-
iosBundleSize: bundleSizes.ios,
|
|
714
|
-
androidBundleSize: bundleSizes.android || bundleSizes.single,
|
|
715
|
-
}, {
|
|
786
|
+
iosBundleSize: bundleSizes.ios, androidBundleSize: bundleSizes.android || bundleSizes.single }), {
|
|
716
787
|
headers: {
|
|
717
788
|
"x-publish-key": authKey,
|
|
718
789
|
"Content-Type": "application/json",
|
|
@@ -746,7 +817,16 @@ const release = new commander_1.Command("release")
|
|
|
746
817
|
if (releaseData.binaryVersionRange) {
|
|
747
818
|
console.log(` Version Range: ${releaseData.binaryVersionRange}`);
|
|
748
819
|
}
|
|
820
|
+
if (opts.minBinaryVersion) {
|
|
821
|
+
console.log(` Min Binary Version: ${chalk_1.default.cyan(opts.minBinaryVersion)}`);
|
|
822
|
+
}
|
|
749
823
|
console.log(chalk_1.default.gray("──────────────────────────────────"));
|
|
824
|
+
// Save git hash after successful release for next detection run
|
|
825
|
+
const gitStatus = (0, native_detector_1.getGitRepoStatus)();
|
|
826
|
+
if (gitStatus) {
|
|
827
|
+
configStore_1.configStore.saveLastReleaseHash(gitStatus.currentHash, opts.label);
|
|
828
|
+
console.log(chalk_1.default.gray(` ⓘ Saved git snapshot for next release detection`));
|
|
829
|
+
}
|
|
750
830
|
}
|
|
751
831
|
catch (error) {
|
|
752
832
|
console.error(chalk_1.default.red(`\n✗ Failed to upload release: ${error.message}`));
|
|
@@ -69,4 +69,18 @@ export declare const configStore: {
|
|
|
69
69
|
* Check if authenticated
|
|
70
70
|
*/
|
|
71
71
|
isAuthenticated(): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Save the git commit hash and release label from the last successful release.
|
|
74
|
+
* Used by the native change detector on the next release run.
|
|
75
|
+
*/
|
|
76
|
+
saveLastReleaseHash(hash: string, label: string): void;
|
|
77
|
+
/**
|
|
78
|
+
* Get the git commit hash saved from the last release.
|
|
79
|
+
* Returns null if no release has been made yet.
|
|
80
|
+
*/
|
|
81
|
+
getLastReleaseHash(): string | null;
|
|
82
|
+
/**
|
|
83
|
+
* Get the release label saved from the last release (for display).
|
|
84
|
+
*/
|
|
85
|
+
getLastReleaseLabel(): string | null;
|
|
72
86
|
};
|
|
@@ -171,4 +171,30 @@ exports.configStore = {
|
|
|
171
171
|
isAuthenticated() {
|
|
172
172
|
return this.getCredentials().length > 0;
|
|
173
173
|
},
|
|
174
|
+
// ============================================
|
|
175
|
+
// Native change detection helpers
|
|
176
|
+
// ============================================
|
|
177
|
+
/**
|
|
178
|
+
* Save the git commit hash and release label from the last successful release.
|
|
179
|
+
* Used by the native change detector on the next release run.
|
|
180
|
+
*/
|
|
181
|
+
saveLastReleaseHash(hash, label) {
|
|
182
|
+
const config = readConfig();
|
|
183
|
+
config.lastReleaseHash = hash;
|
|
184
|
+
config.lastReleaseLabel = label;
|
|
185
|
+
writeConfig(config);
|
|
186
|
+
},
|
|
187
|
+
/**
|
|
188
|
+
* Get the git commit hash saved from the last release.
|
|
189
|
+
* Returns null if no release has been made yet.
|
|
190
|
+
*/
|
|
191
|
+
getLastReleaseHash() {
|
|
192
|
+
return readConfig().lastReleaseHash || null;
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Get the release label saved from the last release (for display).
|
|
196
|
+
*/
|
|
197
|
+
getLastReleaseLabel() {
|
|
198
|
+
return readConfig().lastReleaseLabel || null;
|
|
199
|
+
},
|
|
174
200
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Change Detector
|
|
3
|
+
*
|
|
4
|
+
* Uses git diff to detect changes in native project files since the last
|
|
5
|
+
* Upflux release. Helps developers know when to set --min-binary-version.
|
|
6
|
+
*
|
|
7
|
+
* If git is not initialized or not available, detection is silently skipped.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* A single detected native change
|
|
11
|
+
*/
|
|
12
|
+
export interface NativeChange {
|
|
13
|
+
category: 'android' | 'ios' | 'dependencies';
|
|
14
|
+
file: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Result of native change detection
|
|
19
|
+
*/
|
|
20
|
+
export interface DetectionResult {
|
|
21
|
+
hasNativeChanges: boolean;
|
|
22
|
+
changes: NativeChange[];
|
|
23
|
+
/** Extracted from build.gradle versionName +line — used as suggested min version */
|
|
24
|
+
suggestedMinBinaryVersion?: string;
|
|
25
|
+
/** e.g., ">=2.0.0" derived from suggestedMinBinaryVersion */
|
|
26
|
+
suggestedVersionRange?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if git is available and the CWD is a git repository.
|
|
30
|
+
*
|
|
31
|
+
* Returns the current HEAD hash if git is usable.
|
|
32
|
+
* Returns null if git is not available or the directory is not a git repo —
|
|
33
|
+
* callers should skip detection entirely in this case.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getGitRepoStatus(): {
|
|
36
|
+
currentHash: string;
|
|
37
|
+
} | null;
|
|
38
|
+
/**
|
|
39
|
+
* Run git diff between savedHash and HEAD for native files.
|
|
40
|
+
* Parses added lines (+) to extract structured, meaningful changes.
|
|
41
|
+
*
|
|
42
|
+
* @param savedHash - The git commit hash from the previous release
|
|
43
|
+
*/
|
|
44
|
+
export declare function detectNativeChanges(savedHash: string): DetectionResult;
|
|
45
|
+
/**
|
|
46
|
+
* Pretty-print the detected native changes to the terminal.
|
|
47
|
+
*/
|
|
48
|
+
export declare function displayNativeChangeWarning(result: DetectionResult): void;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Native Change Detector
|
|
4
|
+
*
|
|
5
|
+
* Uses git diff to detect changes in native project files since the last
|
|
6
|
+
* Upflux release. Helps developers know when to set --min-binary-version.
|
|
7
|
+
*
|
|
8
|
+
* If git is not initialized or not available, detection is silently skipped.
|
|
9
|
+
*/
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.getGitRepoStatus = getGitRepoStatus;
|
|
15
|
+
exports.detectNativeChanges = detectNativeChanges;
|
|
16
|
+
exports.displayNativeChangeWarning = displayNativeChangeWarning;
|
|
17
|
+
const child_process_1 = require("child_process");
|
|
18
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
19
|
+
/**
|
|
20
|
+
* Known native dependency prefixes for package.json scanning
|
|
21
|
+
*/
|
|
22
|
+
const KNOWN_NATIVE_PREFIXES = [
|
|
23
|
+
'react-native-',
|
|
24
|
+
'@react-native-community/',
|
|
25
|
+
'@react-native/',
|
|
26
|
+
'expo-',
|
|
27
|
+
'@react-native-firebase/',
|
|
28
|
+
'@react-native-async-storage/',
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Native files to watch via git diff
|
|
32
|
+
*/
|
|
33
|
+
const NATIVE_FILES = [
|
|
34
|
+
'android/app/build.gradle',
|
|
35
|
+
'android/app/src/main/AndroidManifest.xml',
|
|
36
|
+
'ios/Podfile',
|
|
37
|
+
'ios/Podfile.lock',
|
|
38
|
+
'package.json',
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Check if git is available and the CWD is a git repository.
|
|
42
|
+
*
|
|
43
|
+
* Returns the current HEAD hash if git is usable.
|
|
44
|
+
* Returns null if git is not available or the directory is not a git repo —
|
|
45
|
+
* callers should skip detection entirely in this case.
|
|
46
|
+
*/
|
|
47
|
+
function getGitRepoStatus() {
|
|
48
|
+
try {
|
|
49
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
50
|
+
const hash = (0, child_process_1.execSync)('git rev-parse HEAD', {
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
stdio: 'pipe',
|
|
53
|
+
}).trim();
|
|
54
|
+
return { currentHash: hash };
|
|
55
|
+
}
|
|
56
|
+
catch (_a) {
|
|
57
|
+
// Not a git repo or git is not installed
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run git diff between savedHash and HEAD for native files.
|
|
63
|
+
* Parses added lines (+) to extract structured, meaningful changes.
|
|
64
|
+
*
|
|
65
|
+
* @param savedHash - The git commit hash from the previous release
|
|
66
|
+
*/
|
|
67
|
+
function detectNativeChanges(savedHash) {
|
|
68
|
+
let diffOutput = '';
|
|
69
|
+
try {
|
|
70
|
+
diffOutput = (0, child_process_1.execSync)(`git diff ${savedHash}..HEAD -- ${NATIVE_FILES.join(' ')}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
71
|
+
}
|
|
72
|
+
catch (_a) {
|
|
73
|
+
// git diff failed (e.g., hash no longer valid in shallow clone) — skip
|
|
74
|
+
return { hasNativeChanges: false, changes: [] };
|
|
75
|
+
}
|
|
76
|
+
if (!diffOutput.trim()) {
|
|
77
|
+
return { hasNativeChanges: false, changes: [] };
|
|
78
|
+
}
|
|
79
|
+
const changes = [];
|
|
80
|
+
let currentFile = '';
|
|
81
|
+
let suggestedMinBinaryVersion;
|
|
82
|
+
for (const line of diffOutput.split('\n')) {
|
|
83
|
+
// Track which file we are currently diffing
|
|
84
|
+
if (line.startsWith('+++ b/')) {
|
|
85
|
+
currentFile = line.replace('+++ b/', '');
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Only inspect added lines; skip diff metadata lines
|
|
89
|
+
if (!line.startsWith('+') || line.startsWith('+++'))
|
|
90
|
+
continue;
|
|
91
|
+
const added = line.slice(1);
|
|
92
|
+
// ── android/app/build.gradle ──────────────────────────────────────
|
|
93
|
+
if (currentFile === 'android/app/build.gradle') {
|
|
94
|
+
const versionMatch = added.match(/versionName\s+["']([^"']+)["']/);
|
|
95
|
+
if (versionMatch) {
|
|
96
|
+
suggestedMinBinaryVersion = versionMatch[1];
|
|
97
|
+
changes.push({
|
|
98
|
+
category: 'android',
|
|
99
|
+
file: 'build.gradle',
|
|
100
|
+
description: `versionName → ${versionMatch[1]}`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const minSdkMatch = added.match(/minSdkVersion\s+(\d+)/);
|
|
104
|
+
if (minSdkMatch) {
|
|
105
|
+
changes.push({
|
|
106
|
+
category: 'android',
|
|
107
|
+
file: 'build.gradle',
|
|
108
|
+
description: `minSdkVersion → ${minSdkMatch[1]}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ── android/app/src/main/AndroidManifest.xml ──────────────────────
|
|
113
|
+
if (currentFile === 'android/app/src/main/AndroidManifest.xml') {
|
|
114
|
+
const permMatch = added.match(/uses-permission.*android:name="([^"]+)"/);
|
|
115
|
+
if (permMatch) {
|
|
116
|
+
changes.push({
|
|
117
|
+
category: 'android',
|
|
118
|
+
file: 'AndroidManifest.xml',
|
|
119
|
+
description: `New permission: ${permMatch[1]}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ── ios/Podfile ────────────────────────────────────────────────────
|
|
124
|
+
if (currentFile === 'ios/Podfile') {
|
|
125
|
+
const podMatch = added.match(/^\s*pod\s+["']([^"']+)["']/);
|
|
126
|
+
if (podMatch) {
|
|
127
|
+
changes.push({
|
|
128
|
+
category: 'ios',
|
|
129
|
+
file: 'Podfile',
|
|
130
|
+
description: `New pod: ${podMatch[1]}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ── ios/Podfile.lock ───────────────────────────────────────────────
|
|
135
|
+
if (currentFile === 'ios/Podfile.lock') {
|
|
136
|
+
// Any change to the lock file is significant — report only once
|
|
137
|
+
if (!changes.some((c) => c.file === 'Podfile.lock')) {
|
|
138
|
+
changes.push({
|
|
139
|
+
category: 'ios',
|
|
140
|
+
file: 'Podfile.lock',
|
|
141
|
+
description: 'Pod versions changed (Podfile.lock updated)',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ── package.json ───────────────────────────────────────────────────
|
|
146
|
+
if (currentFile === 'package.json') {
|
|
147
|
+
// Match new native dependency entries on added lines
|
|
148
|
+
const depMatch = added.match(/"([^"]+)"\s*:/);
|
|
149
|
+
if (depMatch &&
|
|
150
|
+
KNOWN_NATIVE_PREFIXES.some((prefix) => depMatch[1].startsWith(prefix))) {
|
|
151
|
+
changes.push({
|
|
152
|
+
category: 'dependencies',
|
|
153
|
+
file: 'package.json',
|
|
154
|
+
description: `New native dependency: ${depMatch[1]}`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
hasNativeChanges: changes.length > 0,
|
|
161
|
+
changes,
|
|
162
|
+
suggestedMinBinaryVersion,
|
|
163
|
+
suggestedVersionRange: suggestedMinBinaryVersion
|
|
164
|
+
? `>=${suggestedMinBinaryVersion}`
|
|
165
|
+
: undefined,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Pretty-print the detected native changes to the terminal.
|
|
170
|
+
*/
|
|
171
|
+
function displayNativeChangeWarning(result) {
|
|
172
|
+
var _a;
|
|
173
|
+
console.log(chalk_1.default.yellow('\n⚠️ Native changes detected since last release:\n'));
|
|
174
|
+
// Group by category for cleaner output
|
|
175
|
+
const byCategory = {};
|
|
176
|
+
for (const change of result.changes) {
|
|
177
|
+
(byCategory[_a = change.category] || (byCategory[_a] = [])).push(change);
|
|
178
|
+
}
|
|
179
|
+
for (const [category, items] of Object.entries(byCategory)) {
|
|
180
|
+
const label = category.charAt(0).toUpperCase() + category.slice(1);
|
|
181
|
+
console.log(chalk_1.default.bold(` ${label}:`));
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
console.log(chalk_1.default.yellow(` • ${item.file}: ${item.description}`));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
console.log();
|
|
187
|
+
if (result.suggestedMinBinaryVersion) {
|
|
188
|
+
console.log(chalk_1.default.cyan(' 💡 These changes likely require a new binary from the app store.'));
|
|
189
|
+
console.log(chalk_1.default.cyan(' Suggested flags:'));
|
|
190
|
+
console.log(chalk_1.default.cyan(` --min-binary-version ${result.suggestedMinBinaryVersion}`));
|
|
191
|
+
console.log(chalk_1.default.cyan(` --version-range "${result.suggestedVersionRange}"`));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(chalk_1.default.cyan(' 💡 Consider setting --min-binary-version and --version-range'));
|
|
195
|
+
console.log(chalk_1.default.cyan(' if these changes require a new binary from the app store.'));
|
|
196
|
+
}
|
|
197
|
+
console.log();
|
|
198
|
+
}
|