@siranjeevan/releaseflow 1.0.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.
Files changed (4) hide show
  1. package/firebase.js +46 -0
  2. package/index.js +221 -0
  3. package/package.json +29 -0
  4. package/uploader.js +126 -0
package/firebase.js ADDED
@@ -0,0 +1,46 @@
1
+ const admin = require('firebase-admin');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ let db, storage;
6
+
7
+ const CONFIG_PATH = path.join(__dirname, 'config.json');
8
+
9
+ function isConfigured() {
10
+ if (!fs.existsSync(CONFIG_PATH)) return false;
11
+ try {
12
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
13
+ return !!(config.serviceAccountPath && fs.existsSync(config.serviceAccountPath));
14
+ } catch (e) {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ function initFirebase() {
20
+ if (!isConfigured()) {
21
+ throw new Error('Firebase is not configured. Run "myapp configure" first.');
22
+ }
23
+
24
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
25
+ const serviceAccount = require(path.resolve(config.serviceAccountPath));
26
+
27
+ if (!admin.apps.length) {
28
+ admin.initializeApp({
29
+ credential: admin.credential.cert(serviceAccount),
30
+ storageBucket: config.storageBucket
31
+ });
32
+ }
33
+
34
+ db = admin.firestore();
35
+ storage = admin.storage().bucket();
36
+
37
+ return { db, storage, admin };
38
+ }
39
+
40
+ async function getLiveVersion() {
41
+ const { db } = initFirebase();
42
+ const doc = await db.collection('app_config').doc('version').get();
43
+ return doc.exists ? doc.data().latest_version : '0.0.0';
44
+ }
45
+
46
+ module.exports = { initFirebase, isConfigured, CONFIG_PATH, getLiveVersion };
package/index.js ADDED
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+
3
+ const inquirer = require('inquirer');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const yaml = require('js-yaml');
7
+ const semver = require('semver');
8
+ const { spawn } = require('child_process');
9
+ const {
10
+ uploadApk,
11
+ updateFirestore,
12
+ listAvailableVersions,
13
+ rollbackToVersion
14
+ } = require('./uploader');
15
+ const { isConfigured, CONFIG_PATH, getLiveVersion } = require('./firebase');
16
+
17
+ const yargs = require('yargs/yargs');
18
+ const { hideBin } = require('yargs/helpers');
19
+
20
+ async function main() {
21
+ yargs(hideBin(process.argv))
22
+ .command('release', 'Build, Detect and Upload in ONE command!', {}, async () => {
23
+ console.log('\x1b[36m%s\x1b[0m', '\n🚀 --- ReleaseFlow All-in-One Generator --- 🚀');
24
+ if (!isConfigured()) {
25
+ console.log('\x1b[33mFirebase is not configured yet.\x1b[0m');
26
+ await runConfigureFlow();
27
+ }
28
+ await runFullReleaseFlow();
29
+ })
30
+ .command('rollback', 'Rollback to a previous APK version', {}, async () => {
31
+ console.log('\x1b[36m%s\x1b[0m', '\n⏪ --- Rollback to Previous Version --- ⏪');
32
+ if (!isConfigured()) {
33
+ await runConfigureFlow();
34
+ }
35
+ await runRollbackFlow();
36
+ })
37
+ .command('configure', 'Set up Firebase credentials', {}, async () => {
38
+ console.log('\x1b[36m%s\x1b[0m', '\n--- Configure Firebase Credentials ---');
39
+ await runConfigureFlow();
40
+ })
41
+ .demandCommand(1, 'Please specify a command (e.g., release, rollback, configure)')
42
+ .help()
43
+ .argv;
44
+ }
45
+
46
+ const PUBSPEC_PATH = path.resolve(__dirname, '../flutter_app/pubspec.yaml');
47
+
48
+ function getPubspecVersion() {
49
+ try {
50
+ if (fs.existsSync(PUBSPEC_PATH)) {
51
+ const doc = yaml.load(fs.readFileSync(PUBSPEC_PATH, 'utf8'));
52
+ return doc.version.split('+')[0];
53
+ }
54
+ } catch (e) {
55
+ console.error('Warning: Could not read version from pubspec.yaml');
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function updatePubspecVersion(newVersion) {
61
+ try {
62
+ const rawPubspec = fs.readFileSync(PUBSPEC_PATH, 'utf8');
63
+ const updatedPubspec = rawPubspec.replace(/^version: ([\d\.]+)(.*)$/m, (match, v, build) => {
64
+ return `version: ${newVersion}${build || '+1'}`;
65
+ });
66
+ fs.writeFileSync(PUBSPEC_PATH, updatedPubspec);
67
+ return true;
68
+ } catch (e) {
69
+ console.error('Error updating pubspec.yaml: ' + e.message);
70
+ return false;
71
+ }
72
+ }
73
+
74
+ async function runConfigureFlow() {
75
+ const answers = await inquirer.prompt([
76
+ {
77
+ type: 'input',
78
+ name: 'serviceAccountPath',
79
+ message: 'Enter the path to your Firebase Service Account JSON file:',
80
+ validate: (input) => {
81
+ const fullPath = path.resolve(process.cwd(), input);
82
+ if (fs.existsSync(fullPath) && (input.endsWith('.json') || fs.statSync(fullPath).isFile())) {
83
+ return true;
84
+ }
85
+ return 'Please provide a valid path to your service account .json file.';
86
+ }
87
+ },
88
+ {
89
+ type: 'input',
90
+ name: 'storageBucket',
91
+ message: 'Enter your Firebase Storage Bucket name:',
92
+ default: 'testing-dhwayam.firebasestorage.app'
93
+ }
94
+ ]);
95
+
96
+ const config = {
97
+ serviceAccountPath: path.resolve(process.cwd(), answers.serviceAccountPath),
98
+ storageBucket: answers.storageBucket
99
+ };
100
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
101
+ console.log('\x1b[32m✔ Configuration saved successfully!\x1b[0m\n');
102
+ }
103
+
104
+ async function runFullReleaseFlow() {
105
+ console.log('\x1b[34mChecking versions and history...\x1b[0m');
106
+
107
+ const liveVersion = await getLiveVersion();
108
+ const allVersions = await listAvailableVersions();
109
+ const maxVersion = allVersions.length > 0 ? allVersions[0] : '0.0.0';
110
+ let localVersion = getPubspecVersion();
111
+
112
+ console.log(`- Current Live Version: \x1b[33m${liveVersion}\x1b[0m`);
113
+ console.log(`- Highest Released Version: \x1b[33m${maxVersion}\x1b[0m`);
114
+ console.log(`- Version in pubspec.yaml: \x1b[33m${localVersion}\x1b[0m`);
115
+
116
+ // Logic: If local version is already in storage, suggest a bump
117
+ if (semver.lte(localVersion, maxVersion)) {
118
+ console.log('\n\x1b[33mWarning: This version (or higher) has already been released once!\x1b[0m');
119
+ const suggestedVersion = semver.inc(maxVersion, 'patch');
120
+
121
+ const choice = await inquirer.prompt([{
122
+ type: 'list',
123
+ name: 'action',
124
+ message: `What would you like to do?`,
125
+ choices: [
126
+ { name: `Automatically upgrade code to ${suggestedVersion} and release`, value: 'auto' },
127
+ { name: 'Cancel release', value: 'cancel' }
128
+ ]
129
+ }]);
130
+
131
+ if (choice.action === 'cancel') return;
132
+
133
+ if (choice.action === 'auto') {
134
+ localVersion = suggestedVersion;
135
+ updatePubspecVersion(localVersion);
136
+ console.log(`\x1b[32m✔ Updated pubspec.yaml to ${localVersion}\x1b[0m`);
137
+ }
138
+ } else {
139
+ console.log('\x1b[32m✔ New version detected! Proceeding...\x1b[0m\n');
140
+ }
141
+
142
+ // Automated Build
143
+ console.log('\n\x1b[35m🛠️ Building Optimized APK (arm64)... Please wait...\x1b[0m');
144
+
145
+ const build = spawn('flutter', ['build', 'apk', '--release', '--split-per-abi', '--target-platform', 'android-arm64'], {
146
+ cwd: path.resolve(__dirname, '../flutter_app'),
147
+ stdio: 'inherit'
148
+ });
149
+
150
+ build.on('close', async (code) => {
151
+ if (code !== 0) {
152
+ console.error('\x1b[31m❌ Build failed!\x1b[0m');
153
+ return;
154
+ }
155
+
156
+ console.log('\x1b[32m✔ Build Successful!\x1b[0m');
157
+ const apkPath = '../flutter_app/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk';
158
+ const fullApkPath = path.resolve(__dirname, apkPath);
159
+
160
+ const answers = await inquirer.prompt([{
161
+ type: 'confirm',
162
+ name: 'forceUpdate',
163
+ message: 'Force update for this release?',
164
+ default: false
165
+ }]);
166
+
167
+ try {
168
+ console.log('\n\x1b[33m🚀 Starting Cloud Release Flow...\x1b[0m');
169
+ const downloadUrl = await uploadApk(fullApkPath, localVersion);
170
+ await updateFirestore(localVersion, downloadUrl, answers.forceUpdate);
171
+
172
+ console.log('\n\x1b[35mSummary:\x1b[0m');
173
+ console.log(`Version: ${localVersion}`);
174
+ console.log(`Force Update: ${answers.forceUpdate ? 'Yes' : 'No'}`);
175
+ console.log('\n\x1b[32m🎉 Release Complete! Your app is now LIVE. 🎉\x1b[0m');
176
+ } catch (error) {
177
+ console.error('\x1b[31m\n❌ Error: ' + error.message + '\x1b[0m');
178
+ }
179
+ });
180
+ }
181
+
182
+ async function runRollbackFlow() {
183
+ console.log('\x1b[34mFetching available versions from Storage...\x1b[0m');
184
+ const versions = await listAvailableVersions();
185
+
186
+ if (versions.length === 0) {
187
+ console.log('\x1b[31m❌ No previous versions found.\x1b[0m');
188
+ return;
189
+ }
190
+
191
+ const currentLive = await getLiveVersion();
192
+ console.log(`Current Live Version: \x1b[33m${currentLive}\x1b[0m`);
193
+
194
+ const answers = await inquirer.prompt([
195
+ {
196
+ type: 'list',
197
+ name: 'version',
198
+ message: 'Select a version to rollback to:',
199
+ choices: versions.map(v => ({
200
+ name: v === currentLive ? `${v} (Already Live)` : v,
201
+ value: v
202
+ }))
203
+ },
204
+ {
205
+ type: 'confirm',
206
+ name: 'forceUpdate',
207
+ message: 'Force update for this rollback?',
208
+ default: true
209
+ }
210
+ ]);
211
+
212
+ try {
213
+ console.log(`\n\x1b[33m⏪ Rolling back to version ${answers.version}...\x1b[0m`);
214
+ await rollbackToVersion(answers.version, answers.forceUpdate);
215
+ console.log('\n\x1b[32m✔ Success! The database has been rolled back.\x1b[0m');
216
+ } catch (error) {
217
+ console.error('\n\x1b[31m❌ Rollback failed: ' + error.message + '\x1b[0m');
218
+ }
219
+ }
220
+
221
+ main();
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@siranjeevan/releaseflow",
3
+ "version": "1.0.0",
4
+ "description": "Automated Flutter Release Manager",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "releaseflow": "index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "flutter",
14
+ "release",
15
+ "firebase",
16
+ "automation"
17
+ ],
18
+ "author": "Jeevith",
19
+ "license": "ISC",
20
+ "dependencies": {
21
+ "axios": "^1.7.9",
22
+ "cli-progress": "^3.12.0",
23
+ "firebase-admin": "^13.0.1",
24
+ "inquirer": "^8.0.0",
25
+ "js-yaml": "^4.1.0",
26
+ "semver": "^7.7.1",
27
+ "yargs": "^17.7.2"
28
+ }
29
+ }
package/uploader.js ADDED
@@ -0,0 +1,126 @@
1
+ const { initFirebase } = require('./firebase.js');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const semver = require('semver');
5
+
6
+ async function cleanupOldReleases(storage) {
7
+ try {
8
+ const [files] = await storage.getFiles({ prefix: 'releases/' });
9
+
10
+ const releaseFiles = files
11
+ .map(file => {
12
+ // Improved regex to catch 1.0.4, 1.0.4+1, etc.
13
+ const match = file.name.match(/app-v([\d\.\+\w]+)\.apk/);
14
+ return match ? { file, version: match[1] } : null;
15
+ })
16
+ .filter(item => item !== null);
17
+
18
+ releaseFiles.sort((a, b) => {
19
+ try {
20
+ // Clean version for semver comparison (1.0.4+1 -> 1.0.4)
21
+ const vA = a.version.split('+')[0];
22
+ const vB = b.version.split('+')[0];
23
+ return semver.rcompare(vA, vB);
24
+ } catch (e) {
25
+ return 0;
26
+ }
27
+ });
28
+
29
+ if (releaseFiles.length > 3) {
30
+ console.log(`\n\x1b[34mCleaning up storage: keeping 3 latest history APKs, deleting oldest...\x1b[0m`);
31
+ const filesToDelete = releaseFiles.slice(3);
32
+ for (const item of filesToDelete) {
33
+ console.log(`- Removing obsolete APK: ${item.file.name}`);
34
+ await item.file.delete();
35
+ }
36
+ }
37
+ } catch (error) {
38
+ console.warn('\x1b[33mWarning: Storage cleanup failed: ' + error.message + '\x1b[0m');
39
+ }
40
+ }
41
+
42
+ async function uploadApk(apkPath, version) {
43
+ const { storage } = initFirebase();
44
+ const fileName = `releases/app-v${version}.apk`;
45
+ const destination = fileName;
46
+
47
+ console.info(`\x1b[34mUploading APK to: ${destination}...\x1b[0m`);
48
+
49
+ await storage.upload(apkPath, {
50
+ destination,
51
+ metadata: {
52
+ contentType: 'application/vnd.android.package-archive',
53
+ },
54
+ });
55
+
56
+ await cleanupOldReleases(storage);
57
+
58
+ const file = storage.file(destination);
59
+ const [url] = await file.getSignedUrl({
60
+ action: 'read',
61
+ expires: '03-01-2099',
62
+ });
63
+
64
+ return url;
65
+ }
66
+
67
+ async function updateFirestore(version, url, forceUpdate) {
68
+ const { db } = initFirebase();
69
+ const configRef = db.collection('app_config').doc('version');
70
+
71
+ console.info(`\x1b[34mUpdating Firestore status to: ${version}...\x1b[0m`);
72
+
73
+ await configRef.set({
74
+ latest_version: version,
75
+ apk_url: url,
76
+ force_update: forceUpdate,
77
+ updated_at: new Date().toISOString()
78
+ });
79
+
80
+ return true;
81
+ }
82
+
83
+ async function listAvailableVersions() {
84
+ const { storage } = initFirebase();
85
+ const [files] = await storage.getFiles({ prefix: 'releases/' });
86
+
87
+ return files
88
+ .map(file => {
89
+ const match = file.name.match(/app-v([\d\.\+\w]+)\.apk/);
90
+ return match ? match[1] : null;
91
+ })
92
+ .filter(v => v !== null)
93
+ .sort((a, b) => {
94
+ try {
95
+ return semver.rcompare(a.split('+')[0], b.split('+')[0]);
96
+ } catch (e) { return 0; }
97
+ });
98
+ }
99
+
100
+ async function rollbackToVersion(version, forceUpdate) {
101
+ const { storage, db } = initFirebase();
102
+ const fileName = `releases/app-v${version}.apk`;
103
+ const file = storage.file(fileName);
104
+
105
+ const [exists] = await file.exists();
106
+ if (!exists) {
107
+ throw new Error(`Version ${version} does not exist in Storage.`);
108
+ }
109
+
110
+ const [url] = await file.getSignedUrl({
111
+ action: 'read',
112
+ expires: '03-01-2099',
113
+ });
114
+
115
+ const configRef = db.collection('app_config').doc('version');
116
+ await configRef.set({
117
+ latest_version: version,
118
+ apk_url: url,
119
+ force_update: forceUpdate,
120
+ updated_at: new Date().toISOString()
121
+ });
122
+
123
+ return url;
124
+ }
125
+
126
+ module.exports = { uploadApk, updateFirestore, listAvailableVersions, rollbackToVersion };