pumuki-ast-hooks 5.5.31 ā 5.5.35
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 +162 -1
- package/package.json +11 -2
- package/scripts/hooks-system/application/services/installation/ConfigurationGeneratorService.js +1 -0
- package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +20 -0
- package/scripts/hooks-system/application/services/installation/McpConfigurator.js +0 -1
- package/scripts/hooks-system/bin/gitflow-cycle.js +496 -0
- package/scripts/hooks-system/bin/gitflow-release.js +343 -0
- package/scripts/hooks-system/config/project.config.json +1 -1
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +7 -1
- package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +7 -1
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
- [Complete Architecture and Workflow](#complete-architecture-and-workflow)
|
|
44
44
|
- [What is it?](#what-is-it)
|
|
45
45
|
- [What problems does it solve?](#what-problems-does-it-solve)
|
|
46
|
+
- [Git Flow Automation](#git-flow-automation)
|
|
46
47
|
- [Features](#features)
|
|
47
48
|
- [Use Cases](#use-cases)
|
|
48
49
|
- [Installation](#installation)
|
|
@@ -510,6 +511,70 @@ See [HOW_IT_WORKS.md](./docs/HOW_IT_WORKS.md#architecture-detection-by-platform)
|
|
|
510
511
|
- **MEDIUM/LOW**: Generate warnings in reports
|
|
511
512
|
- **Configurable**: Adjust levels according to your project
|
|
512
513
|
|
|
514
|
+
### š Git Flow Automation
|
|
515
|
+
|
|
516
|
+
**Complete Git Flow workflow automation** with automatic enforcement:
|
|
517
|
+
|
|
518
|
+
- **Protected Branch Blocking**: Automatically blocks commits and pushes to `main`, `master`, and `develop` branches
|
|
519
|
+
- **Auto-Create Feature Branch**: Automatically creates feature branch when on `develop`/`main` based on changes
|
|
520
|
+
- **Smart Branch Naming**: Generates branch names based on file types (feature/, fix/, chore/, docs/, etc.)
|
|
521
|
+
- **Feature Cycle**: Execute complete Git Flow with `npm run ast:gitflow`
|
|
522
|
+
- **Release Cycle**: Create release PR from develop to main with `npm run ast:release`
|
|
523
|
+
- **Automatic PR Creation**: Creates Pull Requests using GitHub CLI (requires `gh`)
|
|
524
|
+
- **Optional Auto-Merge**: Automatically merges PRs with `--auto-merge` flag
|
|
525
|
+
- **Branch Cleanup**: Automatically deletes merged branches (local and remote)
|
|
526
|
+
- **Branch Synchronization**: Syncs `develop` and `main` with remote
|
|
527
|
+
|
|
528
|
+
**Feature Development Workflow:**
|
|
529
|
+
```bash
|
|
530
|
+
# Work on develop/main (changes detected)
|
|
531
|
+
|
|
532
|
+
# Run complete cycle (auto-creates feature branch)
|
|
533
|
+
npm run ast:gitflow -- --auto-merge
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**Release Workflow:**
|
|
537
|
+
```bash
|
|
538
|
+
# Switch to develop branch
|
|
539
|
+
git checkout develop
|
|
540
|
+
|
|
541
|
+
# Create release PR (develop ā main)
|
|
542
|
+
npm run ast:release -- --auto-merge
|
|
543
|
+
|
|
544
|
+
# Or with version tag
|
|
545
|
+
npm run ast:release -- --tag 5.5.35 --auto-merge
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Branch Naming Logic:**
|
|
549
|
+
- `fix/` - Files containing "fix", "bug", "error"
|
|
550
|
+
- `test/` - Test files or "spec" files
|
|
551
|
+
- `docs/` - Documentation files (README, CHANGELOG)
|
|
552
|
+
- `refactor/` - Files containing "refactor", "cleanup"
|
|
553
|
+
- `ci/` - CI/CD files (workflow, github actions)
|
|
554
|
+
- `chore/` - Config files, package.json
|
|
555
|
+
- `feature/` - Default for other changes
|
|
556
|
+
|
|
557
|
+
**Feature Cycle Options:**
|
|
558
|
+
```bash
|
|
559
|
+
npm run ast:gitflow # Basic cycle (auto-creates branch if needed)
|
|
560
|
+
npm run ast:gitflow -- -m "feat: new feature" # Custom commit message
|
|
561
|
+
npm run ast:gitflow -- --auto-merge # Auto-merge PR
|
|
562
|
+
npm run ast:gitflow -- --skip-cleanup # Skip branch cleanup
|
|
563
|
+
npm run ast:gitflow -- --skip-sync # Skip branch sync
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Release Cycle Options:**
|
|
567
|
+
```bash
|
|
568
|
+
npm run ast:release # Create release PR
|
|
569
|
+
npm run ast:release -- --auto-merge # Auto-merge release PR
|
|
570
|
+
npm run ast:release -- --tag 5.5.35 # Create and push git tag
|
|
571
|
+
npm run ast:release -- --pr-title "Release v5.5.35" # Custom PR title
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Hooks:**
|
|
575
|
+
- `pre-commit`: Blocks commits on protected branches + AST analysis
|
|
576
|
+
- `pre-push`: Blocks push to protected branches + validates naming
|
|
577
|
+
|
|
513
578
|
---
|
|
514
579
|
|
|
515
580
|
## Use Cases
|
|
@@ -568,6 +633,21 @@ hook-watch
|
|
|
568
633
|
hook-status
|
|
569
634
|
```
|
|
570
635
|
|
|
636
|
+
### 6. Git Flow Automation
|
|
637
|
+
|
|
638
|
+
```bash
|
|
639
|
+
# Complete Git Flow cycle
|
|
640
|
+
npm run ast:gitflow
|
|
641
|
+
|
|
642
|
+
# With options
|
|
643
|
+
npm run ast:gitflow -- -m "feat: new feature" --auto-merge
|
|
644
|
+
|
|
645
|
+
# Using MCP tools (from IDE)
|
|
646
|
+
mcp0_auto_complete_gitflow
|
|
647
|
+
mcp0_sync_branches
|
|
648
|
+
mcp0_cleanup_stale_branches
|
|
649
|
+
```
|
|
650
|
+
|
|
571
651
|
---
|
|
572
652
|
|
|
573
653
|
## Quick Start
|
|
@@ -1023,6 +1103,9 @@ hook-predict # Violation prediction
|
|
|
1023
1103
|
hook-playbook # Execute playbook
|
|
1024
1104
|
|
|
1025
1105
|
# Git Flow
|
|
1106
|
+
npm run ast:gitflow # Complete Git Flow cycle
|
|
1107
|
+
npm run ast:gitflow -- -m "msg" # With commit message
|
|
1108
|
+
npm run ast:gitflow -- --auto-merge # Auto-merge PR
|
|
1026
1109
|
gitflow check # Verify Git Flow
|
|
1027
1110
|
gitflow status # Current status
|
|
1028
1111
|
gitflow workflow # View full workflow
|
|
@@ -1033,6 +1116,7 @@ gitflow workflow # View full workflow
|
|
|
1033
1116
|
```bash
|
|
1034
1117
|
npm run audit # Full analysis
|
|
1035
1118
|
npm run install-hooks # Install hooks
|
|
1119
|
+
npm run ast:gitflow # Complete Git Flow cycle
|
|
1036
1120
|
npm test # Run tests
|
|
1037
1121
|
npm run lint # Linter
|
|
1038
1122
|
npm run typecheck # Type checking
|
|
@@ -1065,6 +1149,83 @@ For coding standards, see [CODE_STANDARDS.md](./docs/CODE_STANDARDS.md).
|
|
|
1065
1149
|
|
|
1066
1150
|
## š Recent Changes
|
|
1067
1151
|
|
|
1152
|
+
### Version 5.5.35 (2026-01-04)
|
|
1153
|
+
|
|
1154
|
+
**⨠New Features:**
|
|
1155
|
+
- **Git Flow Release Cycle**: New `npm run ast:release` command for creating release PRs (develop ā main)
|
|
1156
|
+
- **Release Automation**: Automatic PR creation from develop to main with optional auto-merge
|
|
1157
|
+
- **Git Tag Support**: Optional git tag creation with `--tag` flag
|
|
1158
|
+
|
|
1159
|
+
**Technical Details:**
|
|
1160
|
+
- Separation of concerns: `ast:gitflow` for features, `ast:release` for releases
|
|
1161
|
+
- Release cycle steps:
|
|
1162
|
+
1. Validates branch (must be develop)
|
|
1163
|
+
2. Syncs develop with origin
|
|
1164
|
+
3. Syncs main with origin
|
|
1165
|
+
4. Creates PR: develop ā main
|
|
1166
|
+
5. Optionally auto-merges PR
|
|
1167
|
+
6. Optionally creates git tag
|
|
1168
|
+
- Follows Git Flow best practices with clear separation between development and release workflows
|
|
1169
|
+
|
|
1170
|
+
---
|
|
1171
|
+
|
|
1172
|
+
### Version 5.5.34 (2026-01-04)
|
|
1173
|
+
|
|
1174
|
+
**⨠New Features:**
|
|
1175
|
+
- **Git Flow Auto-Create Branch**: Automatically creates feature branch when on `develop`/`main` based on changes
|
|
1176
|
+
- **Smart Branch Naming**: Generates branch names based on file types (feature/, fix/, chore/, docs/, etc.)
|
|
1177
|
+
|
|
1178
|
+
**Technical Details:**
|
|
1179
|
+
- When running `npm run ast:gitflow` on protected branch, script now:
|
|
1180
|
+
- Analyzes changed files to infer branch type
|
|
1181
|
+
- Generates descriptive branch name with timestamp
|
|
1182
|
+
- Creates and switches to new feature branch automatically
|
|
1183
|
+
- Continues with complete Git Flow cycle
|
|
1184
|
+
- Branch naming logic:
|
|
1185
|
+
- `fix/` - Files containing "fix", "bug", "error"
|
|
1186
|
+
- `test/` - Test files or "spec" files
|
|
1187
|
+
- `docs/` - Documentation files (README, CHANGELOG)
|
|
1188
|
+
- `refactor/` - Files containing "refactor", "cleanup"
|
|
1189
|
+
- `ci/` - CI/CD files (workflow, github actions)
|
|
1190
|
+
- `chore/` - Config files, package.json
|
|
1191
|
+
- `feature/` - Default for other changes
|
|
1192
|
+
|
|
1193
|
+
---
|
|
1194
|
+
|
|
1195
|
+
### Version 5.5.33 (2026-01-04)
|
|
1196
|
+
|
|
1197
|
+
**š Bug Fixes:**
|
|
1198
|
+
- **iOS Security Analyzer**: Fixed false positive in `ios.security.missing_ssl_pinning` rule
|
|
1199
|
+
- Now recognizes SSL pinning implementations using `URLSessionDelegate` + `URLAuthenticationChallenge`
|
|
1200
|
+
- **iOS Enterprise Analyzer**: Fixed same false positive in `ios.networking.missing_ssl_pinning` rule
|
|
1201
|
+
|
|
1202
|
+
**Technical Details:**
|
|
1203
|
+
- Previous implementation only checked for `ServerTrustPolicy` or `pinning` keywords
|
|
1204
|
+
- Files implementing SSL pinning with `URLSessionDelegate` were incorrectly flagged
|
|
1205
|
+
- Added detection for `URLSessionDelegate` + `URLAuthenticationChallenge` pattern
|
|
1206
|
+
- This fixes false positives on files like `SSLPinningDelegate.swift` that properly implement SSL pinning
|
|
1207
|
+
|
|
1208
|
+
---
|
|
1209
|
+
|
|
1210
|
+
### Version 5.5.32 (2026-01-04)
|
|
1211
|
+
|
|
1212
|
+
**⨠New Features:**
|
|
1213
|
+
- **Git Flow Automation**: Complete Git Flow cycle with `npm run ast:gitflow`
|
|
1214
|
+
- **Protected Branch Blocking**: Pre-commit hook now blocks commits on `main`, `master`, and `develop`
|
|
1215
|
+
- **Pre-Push Hook**: Automatically installed, blocks push to protected branches and validates naming conventions
|
|
1216
|
+
- **Auto PR Creation**: Creates Pull Requests using GitHub CLI (`gh`)
|
|
1217
|
+
- **Auto-Merge Support**: Optional auto-merge with `--auto-merge` flag
|
|
1218
|
+
- **Branch Cleanup**: Automatically deletes merged branches (local + remote)
|
|
1219
|
+
- **Branch Synchronization**: Syncs `develop` and `main` with remote
|
|
1220
|
+
|
|
1221
|
+
**š§ Improvements:**
|
|
1222
|
+
- Added `ast-gitflow` binary to package.json
|
|
1223
|
+
- Added `ast:gitflow` npm script
|
|
1224
|
+
- Updated GitEnvironmentService to install pre-push hook
|
|
1225
|
+
- Updated pre-commit hook to validate protected branches
|
|
1226
|
+
|
|
1227
|
+
---
|
|
1228
|
+
|
|
1068
1229
|
### Version 5.5.25 (2026-01-04)
|
|
1069
1230
|
|
|
1070
1231
|
**ā” Performance Fix:**
|
|
@@ -1174,7 +1335,7 @@ Developed by **Pumuki TeamĀ®**
|
|
|
1174
1335
|
|
|
1175
1336
|
- **Author**: Juan Carlos Merlos AlbarracĆn (Senior Software Architect - AI-Driven Development)
|
|
1176
1337
|
- **Contact**: freelancemerlos@gmail.com
|
|
1177
|
-
- **Version**: 5.5.
|
|
1338
|
+
- **Version**: 5.5.35
|
|
1178
1339
|
- **Repository**: [GitHub](https://github.com/SwiftEnProfundidad/ast-intelligence-hooks)
|
|
1179
1340
|
|
|
1180
1341
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki-ast-hooks",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.35",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"ast-install": "./bin/install.js",
|
|
11
11
|
"ast-violations": "./bin/violations-api.js",
|
|
12
12
|
"ast-check-version": "./bin/check-version.js",
|
|
13
|
+
"ast-gitflow": "./scripts/hooks-system/bin/gitflow-cycle.js",
|
|
13
14
|
"ai-commit": "./bin/ai-commit.sh",
|
|
14
15
|
"hook-run-orchestrator": "./bin/run-orchestrator.js",
|
|
15
16
|
"hook-watch": "./bin/watch-hooks.js",
|
|
@@ -29,6 +30,8 @@
|
|
|
29
30
|
"gitflow:status": "bash bin/gitflow status",
|
|
30
31
|
"gitflow:workflow": "bash bin/gitflow workflow",
|
|
31
32
|
"gitflow:reset": "bash bin/gitflow reset",
|
|
33
|
+
"ast:gitflow": "node scripts/hooks-system/bin/gitflow-cycle.js",
|
|
34
|
+
"ast:release": "node scripts/hooks-system/bin/gitflow-release.js",
|
|
32
35
|
"violations": "node bin/violations",
|
|
33
36
|
"violations:list": "node bin/violations list",
|
|
34
37
|
"violations:show": "node bin/violations show",
|
|
@@ -41,7 +44,13 @@
|
|
|
41
44
|
"build:ts": "tsc --noEmit",
|
|
42
45
|
"typecheck": "tsc --noEmit",
|
|
43
46
|
"ast:refresh": "node scripts/hooks-system/bin/update-evidence.sh",
|
|
44
|
-
"ast:audit": "node scripts/hooks-system/infrastructure/ast/ast-intelligence.js"
|
|
47
|
+
"ast:audit": "node scripts/hooks-system/infrastructure/ast/ast-intelligence.js",
|
|
48
|
+
"ast:guard:start": "bash scripts/hooks-system/bin/evidence-guard start",
|
|
49
|
+
"ast:guard:stop": "bash scripts/hooks-system/bin/evidence-guard stop",
|
|
50
|
+
"ast:guard:restart": "bash scripts/hooks-system/bin/evidence-guard restart",
|
|
51
|
+
"ast:guard:status": "bash scripts/hooks-system/bin/evidence-guard status",
|
|
52
|
+
"ast:guard:logs": "bash scripts/hooks-system/bin/evidence-guard logs",
|
|
53
|
+
"ast:check-version": "node scripts/hooks-system/bin/check-version.js"
|
|
45
54
|
},
|
|
46
55
|
"keywords": [
|
|
47
56
|
"ast",
|
package/scripts/hooks-system/application/services/installation/ConfigurationGeneratorService.js
CHANGED
|
@@ -116,6 +116,7 @@ class ConfigurationGeneratorService {
|
|
|
116
116
|
packageJson.scripts['ast:guard:status'] = 'bash scripts/hooks-system/bin/evidence-guard status';
|
|
117
117
|
packageJson.scripts['ast:guard:logs'] = 'bash scripts/hooks-system/bin/evidence-guard logs';
|
|
118
118
|
packageJson.scripts['ast:check-version'] = 'node scripts/hooks-system/bin/check-version.js';
|
|
119
|
+
packageJson.scripts['ast:gitflow'] = 'node scripts/hooks-system/bin/gitflow-cycle.js';
|
|
119
120
|
|
|
120
121
|
fs.writeFileSync(projectPackageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
121
122
|
this.logSuccess('npm scripts added');
|
|
@@ -108,6 +108,26 @@ fi
|
|
|
108
108
|
# Change to project root (where package.json is)
|
|
109
109
|
cd "$(git rev-parse --show-toplevel)" || exit 1
|
|
110
110
|
|
|
111
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
112
|
+
# GIT FLOW ENFORCEMENT: Block commits on protected branches
|
|
113
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
114
|
+
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
|
|
115
|
+
|
|
116
|
+
if [[ "$CURRENT_BRANCH" == "main" ]] || [[ "$CURRENT_BRANCH" == "master" ]] || [[ "$CURRENT_BRANCH" == "develop" ]]; then
|
|
117
|
+
echo ""
|
|
118
|
+
echo "ā COMMIT BLOCKED: Cannot commit directly to protected branch: $CURRENT_BRANCH"
|
|
119
|
+
echo ""
|
|
120
|
+
echo "š Pumuki says: Create a feature branch first!"
|
|
121
|
+
echo ""
|
|
122
|
+
echo " git checkout -b feature/my-feature"
|
|
123
|
+
echo " git checkout -b fix/my-fix"
|
|
124
|
+
echo " git checkout -b hotfix/urgent-fix"
|
|
125
|
+
echo ""
|
|
126
|
+
echo "Then run: npm run ast:gitflow"
|
|
127
|
+
echo ""
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
|
|
111
131
|
# Check if there are staged files
|
|
112
132
|
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\\.(ts|tsx|js|jsx|swift|kt)$' || true)
|
|
113
133
|
if [ -z "$STAGED_FILES" ]; then
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Git Flow Cycle - Complete automation
|
|
4
|
+
*
|
|
5
|
+
* Executes the full Git Flow cycle:
|
|
6
|
+
* 1. Validates current branch (must be feature/fix/hotfix/chore)
|
|
7
|
+
* 2. Commits uncommitted changes (optional)
|
|
8
|
+
* 3. Pushes to origin
|
|
9
|
+
* 4. Creates PR (requires gh CLI)
|
|
10
|
+
* 5. Optionally merges PR
|
|
11
|
+
* 6. Cleans up merged branches
|
|
12
|
+
* 7. Syncs local branches with remote
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const COLORS = {
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
red: '\x1b[31m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
yellow: '\x1b[33m',
|
|
24
|
+
blue: '\x1b[34m',
|
|
25
|
+
cyan: '\x1b[36m',
|
|
26
|
+
magenta: '\x1b[35m'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function log(color, message) {
|
|
30
|
+
console.log(`${color}${message}${COLORS.reset}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function exec(cmd, options = {}) {
|
|
34
|
+
try {
|
|
35
|
+
return execSync(cmd, {
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
38
|
+
...options
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (options.ignoreError) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function execSilent(cmd) {
|
|
49
|
+
try {
|
|
50
|
+
return execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getCurrentBranch() {
|
|
57
|
+
return execSilent('git branch --show-current') || 'unknown';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isProtectedBranch(branch) {
|
|
61
|
+
const protected = ['main', 'master', 'develop'];
|
|
62
|
+
return protected.includes(branch);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isValidFeatureBranch(branch) {
|
|
66
|
+
return /^(feature|fix|hotfix|chore|docs|refactor|test|ci)\//.test(branch);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasUncommittedChanges() {
|
|
70
|
+
const status = execSilent('git status --porcelain');
|
|
71
|
+
return status && status.length > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function hasStagedChanges() {
|
|
75
|
+
const staged = execSilent('git diff --cached --name-only');
|
|
76
|
+
return staged && staged.length > 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getBaseBranch() {
|
|
80
|
+
// Check if develop exists
|
|
81
|
+
const hasDevelop = execSilent('git show-ref --verify --quiet refs/heads/develop');
|
|
82
|
+
return hasDevelop !== null ? 'develop' : 'main';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getMergedBranches(baseBranch) {
|
|
86
|
+
const output = execSilent(`git branch --merged ${baseBranch}`);
|
|
87
|
+
if (!output) return [];
|
|
88
|
+
return output.split('\n')
|
|
89
|
+
.map(b => b.replace(/^\*?\s*/, '').trim())
|
|
90
|
+
.filter(b => b && !isProtectedBranch(b));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isGitHubCliAvailable() {
|
|
94
|
+
return execSilent('gh auth status') !== null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function prExists(branch) {
|
|
98
|
+
return execSilent(`gh pr view ${branch}`) !== null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
102
|
+
// MAIN CYCLE STEPS
|
|
103
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
104
|
+
|
|
105
|
+
function getChangedFiles() {
|
|
106
|
+
try {
|
|
107
|
+
const output = execSilent('git status --porcelain');
|
|
108
|
+
if (!output) return [];
|
|
109
|
+
return output.split('\n')
|
|
110
|
+
.filter(line => line.trim())
|
|
111
|
+
.map(line => {
|
|
112
|
+
const parts = line.trim().split(/\s+/);
|
|
113
|
+
return parts[1] || parts[0];
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function inferBranchType(changedFiles) {
|
|
121
|
+
const fileTypes = changedFiles.join(' ').toLowerCase();
|
|
122
|
+
|
|
123
|
+
if (fileTypes.includes('fix') || fileTypes.includes('bug') || fileTypes.includes('error')) {
|
|
124
|
+
return 'fix';
|
|
125
|
+
}
|
|
126
|
+
if (fileTypes.includes('test') || fileTypes.includes('spec')) {
|
|
127
|
+
return 'test';
|
|
128
|
+
}
|
|
129
|
+
if (fileTypes.includes('doc') || fileTypes.includes('readme') || fileTypes.includes('changelog')) {
|
|
130
|
+
return 'docs';
|
|
131
|
+
}
|
|
132
|
+
if (fileTypes.includes('refactor') || fileTypes.includes('cleanup')) {
|
|
133
|
+
return 'refactor';
|
|
134
|
+
}
|
|
135
|
+
if (fileTypes.includes('ci') || fileTypes.includes('workflow') || fileTypes.includes('github')) {
|
|
136
|
+
return 'ci';
|
|
137
|
+
}
|
|
138
|
+
if (fileTypes.includes('chore') || fileTypes.includes('config') || fileTypes.includes('package')) {
|
|
139
|
+
return 'chore';
|
|
140
|
+
}
|
|
141
|
+
return 'feature';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function generateBranchName(changedFiles) {
|
|
145
|
+
const type = inferBranchType(changedFiles);
|
|
146
|
+
|
|
147
|
+
// Extract keywords from file paths
|
|
148
|
+
const keywords = changedFiles
|
|
149
|
+
.map(f => f.replace(/.*\//, '').replace(/\.[^.]+$/, ''))
|
|
150
|
+
.filter(f => f.length > 2)
|
|
151
|
+
.slice(0, 3);
|
|
152
|
+
|
|
153
|
+
let suffix = keywords.join('-') || 'update';
|
|
154
|
+
suffix = suffix.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
155
|
+
|
|
156
|
+
// Add timestamp to ensure uniqueness
|
|
157
|
+
const timestamp = Date.now().toString(36);
|
|
158
|
+
return `${type}/${suffix}-${timestamp}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function step1_validateBranch() {
|
|
162
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
163
|
+
log(COLORS.cyan, 'š Step 1: Validate Branch');
|
|
164
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
165
|
+
|
|
166
|
+
let branch = getCurrentBranch();
|
|
167
|
+
log(COLORS.blue, `Current branch: ${branch}`);
|
|
168
|
+
|
|
169
|
+
if (isProtectedBranch(branch)) {
|
|
170
|
+
log(COLORS.yellow, `\nā ļø On protected branch: ${branch}`);
|
|
171
|
+
log(COLORS.cyan, 'š Automatically creating feature branch...');
|
|
172
|
+
|
|
173
|
+
const changedFiles = getChangedFiles();
|
|
174
|
+
if (changedFiles.length === 0) {
|
|
175
|
+
log(COLORS.red, '\nā No changes detected to create a feature branch');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const newBranch = generateBranchName(changedFiles);
|
|
180
|
+
log(COLORS.blue, `Creating branch: ${newBranch}`);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
exec(`git checkout -b ${newBranch}`);
|
|
184
|
+
branch = newBranch;
|
|
185
|
+
log(COLORS.green, `ā
Created and switched to: ${newBranch}`);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
log(COLORS.red, `\nā Failed to create branch: ${error.message}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!isValidFeatureBranch(branch)) {
|
|
193
|
+
log(COLORS.yellow, `\nā ļø Branch '${branch}' doesn't follow naming convention`);
|
|
194
|
+
log(COLORS.yellow, 'Expected: feature/, fix/, hotfix/, chore/, docs/, refactor/, test/, ci/');
|
|
195
|
+
log(COLORS.yellow, '\nContinuing anyway...');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
log(COLORS.green, 'ā
Branch validation passed');
|
|
199
|
+
return branch;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function step2_commitChanges(commitMessage) {
|
|
203
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
204
|
+
log(COLORS.cyan, 'š Step 2: Commit Changes');
|
|
205
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
206
|
+
|
|
207
|
+
if (!hasUncommittedChanges()) {
|
|
208
|
+
log(COLORS.green, 'ā
No uncommitted changes');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
log(COLORS.yellow, 'ā ļø Uncommitted changes detected');
|
|
213
|
+
|
|
214
|
+
if (!hasStagedChanges()) {
|
|
215
|
+
log(COLORS.blue, 'Staging all changes...');
|
|
216
|
+
exec('git add -A');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const message = commitMessage || `chore: auto-commit changes on ${getCurrentBranch()}`;
|
|
220
|
+
log(COLORS.blue, `Committing: ${message}`);
|
|
221
|
+
exec(`git commit -m "${message}"`);
|
|
222
|
+
log(COLORS.green, 'ā
Changes committed');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function step3_pushToOrigin(branch) {
|
|
226
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
227
|
+
log(COLORS.cyan, 'š Step 3: Push to Origin');
|
|
228
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
229
|
+
|
|
230
|
+
log(COLORS.blue, `Pushing ${branch} to origin...`);
|
|
231
|
+
exec(`git push -u origin ${branch}`);
|
|
232
|
+
log(COLORS.green, 'ā
Pushed to origin');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function step4_createPR(branch, baseBranch, prTitle, prBody) {
|
|
236
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
237
|
+
log(COLORS.cyan, 'š Step 4: Create Pull Request');
|
|
238
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
239
|
+
|
|
240
|
+
if (!isGitHubCliAvailable()) {
|
|
241
|
+
log(COLORS.yellow, 'ā ļø GitHub CLI (gh) not available or not authenticated');
|
|
242
|
+
log(COLORS.yellow, ' Install: https://cli.github.com/');
|
|
243
|
+
log(COLORS.yellow, ' Auth: gh auth login');
|
|
244
|
+
log(COLORS.yellow, '\n Create PR manually at GitHub.');
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (prExists(branch)) {
|
|
249
|
+
log(COLORS.green, 'ā
PR already exists for this branch');
|
|
250
|
+
const prUrl = execSilent(`gh pr view ${branch} --json url --jq .url`);
|
|
251
|
+
return prUrl;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const title = prTitle || execSilent('git log -1 --pretty=%s') || `Merge ${branch}`;
|
|
255
|
+
const body = prBody || `## Summary\nAutomated PR for ${branch}\n\n## Changes\n${execSilent(`git log --oneline origin/${baseBranch}..${branch}`) || '- Updates'}`;
|
|
256
|
+
|
|
257
|
+
log(COLORS.blue, `Creating PR: ${title}`);
|
|
258
|
+
log(COLORS.blue, `Base: ${baseBranch} ā Head: ${branch}`);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const output = execSilent(`gh pr create --base ${baseBranch} --head ${branch} --title "${title}" --body "${body.replace(/"/g, '\\"')}"`);
|
|
262
|
+
if (output) {
|
|
263
|
+
const urlMatch = output.match(/https:\/\/github\.com\/.*\/pull\/\d+/);
|
|
264
|
+
const prUrl = urlMatch ? urlMatch[0] : output;
|
|
265
|
+
log(COLORS.green, `ā
PR created: ${prUrl}`);
|
|
266
|
+
return prUrl;
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
log(COLORS.red, `ā Failed to create PR: ${error.message}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function step5_mergePR(prUrl, autoMerge) {
|
|
276
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
277
|
+
log(COLORS.cyan, 'š Step 5: Merge Pull Request');
|
|
278
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
279
|
+
|
|
280
|
+
if (!prUrl) {
|
|
281
|
+
log(COLORS.yellow, 'ā ļø No PR URL available, skipping merge');
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!autoMerge) {
|
|
286
|
+
log(COLORS.yellow, 'ā ļø Auto-merge disabled. Merge PR manually.');
|
|
287
|
+
log(COLORS.blue, ` PR: ${prUrl}`);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
log(COLORS.blue, 'Enabling auto-merge (squash)...');
|
|
292
|
+
try {
|
|
293
|
+
exec(`gh pr merge --auto --squash "${prUrl}"`, { ignoreError: true });
|
|
294
|
+
log(COLORS.green, 'ā
Auto-merge enabled');
|
|
295
|
+
return true;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
log(COLORS.yellow, 'ā ļø Could not enable auto-merge (may require admin approval)');
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function step6_cleanupBranches(baseBranch) {
|
|
303
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
304
|
+
log(COLORS.cyan, 'š§¹ Step 6: Cleanup Merged Branches');
|
|
305
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
306
|
+
|
|
307
|
+
// Fetch latest
|
|
308
|
+
exec('git fetch --prune', { silent: true, ignoreError: true });
|
|
309
|
+
|
|
310
|
+
const mergedBranches = getMergedBranches(baseBranch);
|
|
311
|
+
|
|
312
|
+
if (mergedBranches.length === 0) {
|
|
313
|
+
log(COLORS.green, 'ā
No merged branches to clean');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
log(COLORS.blue, `Found ${mergedBranches.length} merged branches:`);
|
|
318
|
+
mergedBranches.forEach(b => log(COLORS.yellow, ` - ${b}`));
|
|
319
|
+
|
|
320
|
+
// Delete local merged branches
|
|
321
|
+
for (const branch of mergedBranches) {
|
|
322
|
+
log(COLORS.blue, `Deleting local: ${branch}`);
|
|
323
|
+
exec(`git branch -d "${branch}"`, { ignoreError: true, silent: true });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Delete remote merged branches (if gh available)
|
|
327
|
+
if (isGitHubCliAvailable()) {
|
|
328
|
+
for (const branch of mergedBranches) {
|
|
329
|
+
log(COLORS.blue, `Deleting remote: origin/${branch}`);
|
|
330
|
+
exec(`git push origin --delete "${branch}"`, { ignoreError: true, silent: true });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
log(COLORS.green, `ā
Cleaned ${mergedBranches.length} merged branches`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function step7_syncBranches(baseBranch) {
|
|
338
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
339
|
+
log(COLORS.cyan, 'š Step 7: Sync Branches');
|
|
340
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
341
|
+
|
|
342
|
+
const currentBranch = getCurrentBranch();
|
|
343
|
+
|
|
344
|
+
log(COLORS.blue, 'Fetching from origin...');
|
|
345
|
+
exec('git fetch origin', { ignoreError: true, silent: true });
|
|
346
|
+
|
|
347
|
+
// Sync develop
|
|
348
|
+
if (execSilent('git show-ref --verify --quiet refs/heads/develop') !== null) {
|
|
349
|
+
log(COLORS.blue, 'Syncing develop...');
|
|
350
|
+
exec('git checkout develop && git pull origin develop', { ignoreError: true, silent: true });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Sync main
|
|
354
|
+
log(COLORS.blue, 'Syncing main...');
|
|
355
|
+
exec('git checkout main && git pull origin main', { ignoreError: true, silent: true });
|
|
356
|
+
|
|
357
|
+
// Return to original branch
|
|
358
|
+
log(COLORS.blue, `Returning to ${currentBranch}...`);
|
|
359
|
+
exec(`git checkout ${currentBranch}`, { ignoreError: true, silent: true });
|
|
360
|
+
|
|
361
|
+
log(COLORS.green, 'ā
Branches synchronized');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
365
|
+
// MAIN
|
|
366
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
367
|
+
|
|
368
|
+
function printUsage() {
|
|
369
|
+
console.log(`
|
|
370
|
+
${COLORS.cyan}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
371
|
+
š Pumuki Git Flow Cycle
|
|
372
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${COLORS.reset}
|
|
373
|
+
|
|
374
|
+
${COLORS.blue}Usage:${COLORS.reset} npm run ast:gitflow [options]
|
|
375
|
+
|
|
376
|
+
${COLORS.blue}Options:${COLORS.reset}
|
|
377
|
+
--commit-message, -m <msg> Custom commit message
|
|
378
|
+
--pr-title <title> Custom PR title
|
|
379
|
+
--auto-merge Enable auto-merge after PR creation
|
|
380
|
+
--skip-cleanup Skip branch cleanup step
|
|
381
|
+
--skip-sync Skip branch sync step
|
|
382
|
+
--help, -h Show this help
|
|
383
|
+
|
|
384
|
+
${COLORS.blue}Examples:${COLORS.reset}
|
|
385
|
+
npm run ast:gitflow
|
|
386
|
+
npm run ast:gitflow -- -m "feat: add new feature"
|
|
387
|
+
npm run ast:gitflow -- --auto-merge
|
|
388
|
+
npm run ast:gitflow -- --pr-title "My PR Title" --auto-merge
|
|
389
|
+
|
|
390
|
+
${COLORS.blue}What it does:${COLORS.reset}
|
|
391
|
+
1. Validates branch (must be feature/, fix/, etc.)
|
|
392
|
+
2. Commits uncommitted changes
|
|
393
|
+
3. Pushes to origin
|
|
394
|
+
4. Creates PR (requires gh CLI)
|
|
395
|
+
5. Optionally auto-merges PR
|
|
396
|
+
6. Cleans up merged branches
|
|
397
|
+
7. Syncs local branches with remote
|
|
398
|
+
|
|
399
|
+
${COLORS.yellow}ā ļø Requires:${COLORS.reset}
|
|
400
|
+
- Git repository
|
|
401
|
+
- GitHub CLI (gh) for PR operations: https://cli.github.com/
|
|
402
|
+
`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function parseArgs(args) {
|
|
406
|
+
const options = {
|
|
407
|
+
commitMessage: null,
|
|
408
|
+
prTitle: null,
|
|
409
|
+
prBody: null,
|
|
410
|
+
autoMerge: false,
|
|
411
|
+
skipCleanup: false,
|
|
412
|
+
skipSync: false,
|
|
413
|
+
help: false
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
for (let i = 0; i < args.length; i++) {
|
|
417
|
+
const arg = args[i];
|
|
418
|
+
switch (arg) {
|
|
419
|
+
case '--commit-message':
|
|
420
|
+
case '-m':
|
|
421
|
+
options.commitMessage = args[++i];
|
|
422
|
+
break;
|
|
423
|
+
case '--pr-title':
|
|
424
|
+
options.prTitle = args[++i];
|
|
425
|
+
break;
|
|
426
|
+
case '--pr-body':
|
|
427
|
+
options.prBody = args[++i];
|
|
428
|
+
break;
|
|
429
|
+
case '--auto-merge':
|
|
430
|
+
options.autoMerge = true;
|
|
431
|
+
break;
|
|
432
|
+
case '--skip-cleanup':
|
|
433
|
+
options.skipCleanup = true;
|
|
434
|
+
break;
|
|
435
|
+
case '--skip-sync':
|
|
436
|
+
options.skipSync = true;
|
|
437
|
+
break;
|
|
438
|
+
case '--help':
|
|
439
|
+
case '-h':
|
|
440
|
+
options.help = true;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return options;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function main() {
|
|
449
|
+
const args = process.argv.slice(2);
|
|
450
|
+
const options = parseArgs(args);
|
|
451
|
+
|
|
452
|
+
if (options.help) {
|
|
453
|
+
printUsage();
|
|
454
|
+
process.exit(0);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
log(COLORS.magenta, '\nš Pumuki Git Flow Cycle - Starting...\n');
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
// Step 1: Validate branch
|
|
461
|
+
const branch = step1_validateBranch();
|
|
462
|
+
const baseBranch = getBaseBranch();
|
|
463
|
+
|
|
464
|
+
// Step 2: Commit changes
|
|
465
|
+
step2_commitChanges(options.commitMessage);
|
|
466
|
+
|
|
467
|
+
// Step 3: Push to origin
|
|
468
|
+
step3_pushToOrigin(branch);
|
|
469
|
+
|
|
470
|
+
// Step 4: Create PR
|
|
471
|
+
const prUrl = step4_createPR(branch, baseBranch, options.prTitle, options.prBody);
|
|
472
|
+
|
|
473
|
+
// Step 5: Merge PR (optional)
|
|
474
|
+
step5_mergePR(prUrl, options.autoMerge);
|
|
475
|
+
|
|
476
|
+
// Step 6: Cleanup branches
|
|
477
|
+
if (!options.skipCleanup) {
|
|
478
|
+
step6_cleanupBranches(baseBranch);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Step 7: Sync branches
|
|
482
|
+
if (!options.skipSync) {
|
|
483
|
+
step7_syncBranches(baseBranch);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
log(COLORS.green, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
487
|
+
log(COLORS.green, 'ā
Git Flow Cycle Complete!');
|
|
488
|
+
log(COLORS.green, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
489
|
+
|
|
490
|
+
} catch (error) {
|
|
491
|
+
log(COLORS.red, `\nā Git Flow Cycle failed: ${error.message}`);
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
main();
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Git Flow Release Cycle - Release automation
|
|
4
|
+
*
|
|
5
|
+
* Executes the release cycle (develop ā main):
|
|
6
|
+
* 1. Validates current branch (must be develop)
|
|
7
|
+
* 2. Syncs develop with origin
|
|
8
|
+
* 3. Creates PR from develop to main
|
|
9
|
+
* 4. Optionally merges PR
|
|
10
|
+
* 5. Syncs main with origin
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
const COLORS = {
|
|
16
|
+
reset: '\x1b[0m',
|
|
17
|
+
red: '\x1b[31m',
|
|
18
|
+
green: '\x1b[32m',
|
|
19
|
+
yellow: '\x1b[33m',
|
|
20
|
+
blue: '\x1b[34m',
|
|
21
|
+
cyan: '\x1b[36m',
|
|
22
|
+
magenta: '\x1b[35m'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function log(color, message) {
|
|
26
|
+
console.log(`${color}${message}${COLORS.reset}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function exec(cmd, options = {}) {
|
|
30
|
+
try {
|
|
31
|
+
return execSync(cmd, {
|
|
32
|
+
encoding: 'utf-8',
|
|
33
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
34
|
+
...options
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (options.ignoreError) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function execSilent(cmd) {
|
|
45
|
+
try {
|
|
46
|
+
return execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getCurrentBranch() {
|
|
53
|
+
return execSilent('git branch --show-current') || 'unknown';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isProtectedBranch(branch) {
|
|
57
|
+
const protected = ['main', 'master', 'develop'];
|
|
58
|
+
return protected.includes(branch);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasUncommittedChanges() {
|
|
62
|
+
const status = execSilent('git status --porcelain');
|
|
63
|
+
return status && status.length > 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isGitHubCliAvailable() {
|
|
67
|
+
return execSilent('gh auth status') !== null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function prExists(base, head) {
|
|
71
|
+
return execSilent(`gh pr list --base ${base} --head ${head} --json number --jq '. | length'`) !== '0';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getPrUrl(base, head) {
|
|
75
|
+
const output = execSilent(`gh pr list --base ${base} --head ${head} --json url --jq '.[0].url'`);
|
|
76
|
+
return output || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
80
|
+
// MAIN CYCLE STEPS
|
|
81
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
82
|
+
|
|
83
|
+
function step1_validateBranch() {
|
|
84
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
85
|
+
log(COLORS.cyan, 'š Step 1: Validate Branch');
|
|
86
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
87
|
+
|
|
88
|
+
const branch = getCurrentBranch();
|
|
89
|
+
log(COLORS.blue, `Current branch: ${branch}`);
|
|
90
|
+
|
|
91
|
+
if (branch !== 'develop') {
|
|
92
|
+
log(COLORS.red, `\nā Release cycle must be run from develop branch`);
|
|
93
|
+
log(COLORS.yellow, `\nCurrent branch: ${branch}`);
|
|
94
|
+
log(COLORS.yellow, 'Switch to develop first:');
|
|
95
|
+
log(COLORS.yellow, ' git checkout develop');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (hasUncommittedChanges()) {
|
|
100
|
+
log(COLORS.red, '\nā Uncommitted changes detected');
|
|
101
|
+
log(COLORS.yellow, 'Commit or stash changes before creating release');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
log(COLORS.green, 'ā
Branch validation passed');
|
|
106
|
+
return branch;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function step2_syncDevelop() {
|
|
110
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
111
|
+
log(COLORS.cyan, 'š Step 2: Sync Develop');
|
|
112
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
113
|
+
|
|
114
|
+
log(COLORS.blue, 'Fetching from origin...');
|
|
115
|
+
exec('git fetch origin', { silent: true });
|
|
116
|
+
|
|
117
|
+
log(COLORS.blue, 'Pulling latest changes from develop...');
|
|
118
|
+
exec('git pull origin develop', { silent: true });
|
|
119
|
+
|
|
120
|
+
log(COLORS.green, 'ā
Develop synced');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function step3_syncMain() {
|
|
124
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
125
|
+
log(COLORS.cyan, 'š Step 3: Sync Main');
|
|
126
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
127
|
+
|
|
128
|
+
log(COLORS.blue, 'Fetching from origin...');
|
|
129
|
+
exec('git fetch origin', { silent: true });
|
|
130
|
+
|
|
131
|
+
log(COLORS.blue, 'Pulling latest changes from main...');
|
|
132
|
+
exec('git pull origin main', { silent: true });
|
|
133
|
+
|
|
134
|
+
log(COLORS.green, 'ā
Main synced');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function step4_createReleasePR(prTitle, prBody) {
|
|
138
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
139
|
+
log(COLORS.cyan, 'š Step 4: Create Release PR');
|
|
140
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
141
|
+
|
|
142
|
+
if (!isGitHubCliAvailable()) {
|
|
143
|
+
log(COLORS.yellow, 'ā ļø GitHub CLI (gh) not available or not authenticated');
|
|
144
|
+
log(COLORS.yellow, ' Install: https://cli.github.com/');
|
|
145
|
+
log(COLORS.yellow, ' Auth: gh auth login');
|
|
146
|
+
log(COLORS.yellow, '\n Create PR manually at GitHub.');
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const base = 'main';
|
|
151
|
+
const head = 'develop';
|
|
152
|
+
|
|
153
|
+
if (prExists(base, head)) {
|
|
154
|
+
const existingPrUrl = getPrUrl(base, head);
|
|
155
|
+
log(COLORS.green, 'ā
Release PR already exists');
|
|
156
|
+
log(COLORS.blue, ` PR: ${existingPrUrl}`);
|
|
157
|
+
return existingPrUrl;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const title = prTitle || `Release: ${new Date().toISOString().split('T')[0]}`;
|
|
161
|
+
const body = prBody || `## Release Summary\n\nAutomated release from develop to main\n\n## Changes\n${execSilent('git log --oneline origin/main..develop') || '- See commit history'}`;
|
|
162
|
+
|
|
163
|
+
log(COLORS.blue, `Creating release PR: ${title}`);
|
|
164
|
+
log(COLORS.blue, `Base: ${base} ā Head: ${head}`);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const output = execSilent(`gh pr create --base ${base} --head ${head} --title "${title}" --body "${body.replace(/"/g, '\\"')}"`);
|
|
168
|
+
if (output) {
|
|
169
|
+
const urlMatch = output.match(/https:\/\/github\.com\/.*\/pull\/\d+/);
|
|
170
|
+
const prUrl = urlMatch ? urlMatch[0] : output;
|
|
171
|
+
log(COLORS.green, `ā
Release PR created: ${prUrl}`);
|
|
172
|
+
return prUrl;
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
log(COLORS.red, `ā Failed to create PR: ${error.message}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function step5_mergeReleasePR(prUrl, autoMerge) {
|
|
182
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
183
|
+
log(COLORS.cyan, 'š Step 5: Merge Release PR');
|
|
184
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
185
|
+
|
|
186
|
+
if (!prUrl) {
|
|
187
|
+
log(COLORS.yellow, 'ā ļø No PR URL available, skipping merge');
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!autoMerge) {
|
|
192
|
+
log(COLORS.yellow, 'ā ļø Auto-merge disabled. Merge PR manually.');
|
|
193
|
+
log(COLORS.blue, ` PR: ${prUrl}`);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
log(COLORS.blue, 'Enabling auto-merge (squash)...');
|
|
198
|
+
try {
|
|
199
|
+
exec(`gh pr merge --auto --squash "${prUrl}"`, { ignoreError: true });
|
|
200
|
+
log(COLORS.green, 'ā
Auto-merge enabled');
|
|
201
|
+
return true;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
log(COLORS.yellow, 'ā ļø Could not enable auto-merge (may require admin approval)');
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function step6_tagRelease(version) {
|
|
209
|
+
log(COLORS.cyan, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
210
|
+
log(COLORS.cyan, 'š·ļø Step 6: Tag Release');
|
|
211
|
+
log(COLORS.cyan, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
212
|
+
|
|
213
|
+
if (!version) {
|
|
214
|
+
log(COLORS.yellow, 'ā ļø No version specified, skipping tag');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
log(COLORS.blue, `Creating tag: v${version}`);
|
|
219
|
+
try {
|
|
220
|
+
exec(`git tag -a v${version} -m "Release v${version}"`);
|
|
221
|
+
exec(`git push origin v${version}`);
|
|
222
|
+
log(COLORS.green, `ā
Tag v${version} created and pushed`);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
log(COLORS.red, `ā Failed to create tag: ${error.message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
229
|
+
// MAIN
|
|
230
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
231
|
+
|
|
232
|
+
function printUsage() {
|
|
233
|
+
console.log(`
|
|
234
|
+
${COLORS.cyan}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
235
|
+
š Pumuki Git Flow Release Cycle
|
|
236
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${COLORS.reset}
|
|
237
|
+
|
|
238
|
+
${COLORS.blue}Usage:${COLORS.reset} npm run ast:release [options]
|
|
239
|
+
|
|
240
|
+
${COLORS.blue}Options:${COLORS.reset}
|
|
241
|
+
--pr-title <title> Custom PR title
|
|
242
|
+
--pr-body <body> Custom PR body
|
|
243
|
+
--auto-merge Enable auto-merge after PR creation
|
|
244
|
+
--tag <version> Create and push git tag
|
|
245
|
+
--help, -h Show this help
|
|
246
|
+
|
|
247
|
+
${COLORS.blue}Examples:${COLORS.reset}
|
|
248
|
+
npm run ast:release
|
|
249
|
+
npm run ast:release -- --auto-merge
|
|
250
|
+
npm run ast:release -- --tag 5.5.35 --auto-merge
|
|
251
|
+
npm run ast:release -- --pr-title "Release v5.5.35" --auto-merge
|
|
252
|
+
|
|
253
|
+
${COLORS.blue}What it does:${COLORS.reset}
|
|
254
|
+
1. Validates branch (must be develop)
|
|
255
|
+
2. Syncs develop with origin
|
|
256
|
+
3. Syncs main with origin
|
|
257
|
+
4. Creates PR: develop ā main
|
|
258
|
+
5. Optionally auto-merges PR
|
|
259
|
+
6. Optionally creates git tag
|
|
260
|
+
|
|
261
|
+
${COLORS.yellow}ā ļø Requires:${COLORS.reset}
|
|
262
|
+
- Git repository
|
|
263
|
+
- GitHub CLI (gh) for PR operations: https://cli.github.com/
|
|
264
|
+
- Must be on develop branch
|
|
265
|
+
- No uncommitted changes
|
|
266
|
+
`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parseArgs(args) {
|
|
270
|
+
const options = {
|
|
271
|
+
prTitle: null,
|
|
272
|
+
prBody: null,
|
|
273
|
+
autoMerge: false,
|
|
274
|
+
tag: null,
|
|
275
|
+
help: false
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
for (let i = 0; i < args.length; i++) {
|
|
279
|
+
const arg = args[i];
|
|
280
|
+
switch (arg) {
|
|
281
|
+
case '--pr-title':
|
|
282
|
+
options.prTitle = args[++i];
|
|
283
|
+
break;
|
|
284
|
+
case '--pr-body':
|
|
285
|
+
options.prBody = args[++i];
|
|
286
|
+
break;
|
|
287
|
+
case '--auto-merge':
|
|
288
|
+
options.autoMerge = true;
|
|
289
|
+
break;
|
|
290
|
+
case '--tag':
|
|
291
|
+
options.tag = args[++i];
|
|
292
|
+
break;
|
|
293
|
+
case '--help':
|
|
294
|
+
case '-h':
|
|
295
|
+
options.help = true;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return options;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function main() {
|
|
304
|
+
const args = process.argv.slice(2);
|
|
305
|
+
const options = parseArgs(args);
|
|
306
|
+
|
|
307
|
+
if (options.help) {
|
|
308
|
+
printUsage();
|
|
309
|
+
process.exit(0);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
log(COLORS.magenta, '\nš Pumuki Git Flow Release Cycle - Starting...\n');
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
// Step 1: Validate branch
|
|
316
|
+
step1_validateBranch();
|
|
317
|
+
|
|
318
|
+
// Step 2: Sync develop
|
|
319
|
+
step2_syncDevelop();
|
|
320
|
+
|
|
321
|
+
// Step 3: Sync main
|
|
322
|
+
step3_syncMain();
|
|
323
|
+
|
|
324
|
+
// Step 4: Create release PR
|
|
325
|
+
const prUrl = step4_createReleasePR(options.prTitle, options.prBody);
|
|
326
|
+
|
|
327
|
+
// Step 5: Merge PR (optional)
|
|
328
|
+
step5_mergeReleasePR(prUrl, options.autoMerge);
|
|
329
|
+
|
|
330
|
+
// Step 6: Tag release (optional)
|
|
331
|
+
step6_tagRelease(options.tag);
|
|
332
|
+
|
|
333
|
+
log(COLORS.green, '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
334
|
+
log(COLORS.green, 'ā
Git Flow Release Cycle Complete!');
|
|
335
|
+
log(COLORS.green, 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
336
|
+
|
|
337
|
+
} catch (error) {
|
|
338
|
+
log(COLORS.red, `\nā Git Flow Release Cycle failed: ${error.message}`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
main();
|
|
@@ -294,7 +294,13 @@ class iOSEnterpriseAnalyzer {
|
|
|
294
294
|
'Network code without custom NetworkError enum');
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
// Check for SSL pinning implementation
|
|
298
|
+
const hasSSLPinningImplementation =
|
|
299
|
+
content.includes('serverTrustPolicy') ||
|
|
300
|
+
content.includes('pinning') ||
|
|
301
|
+
(content.includes('URLSessionDelegate') && content.includes('URLAuthenticationChallenge'));
|
|
302
|
+
|
|
303
|
+
if (content.includes('URLSession') && !hasSSLPinningImplementation) {
|
|
298
304
|
this.addFinding('ios.networking.missing_ssl_pinning', 'medium', filePath, 1,
|
|
299
305
|
'Consider SSL pinning for high-security apps');
|
|
300
306
|
}
|
|
@@ -1544,7 +1544,13 @@ async function runIOSIntelligence(project, findings, platform) {
|
|
|
1544
1544
|
);
|
|
1545
1545
|
}
|
|
1546
1546
|
|
|
1547
|
-
|
|
1547
|
+
// Check for SSL pinning implementation
|
|
1548
|
+
const hasSSLPinningImplementation =
|
|
1549
|
+
content.includes('ServerTrustPolicy') ||
|
|
1550
|
+
content.includes('pinning') ||
|
|
1551
|
+
(content.includes('URLSessionDelegate') && content.includes('URLAuthenticationChallenge'));
|
|
1552
|
+
|
|
1553
|
+
if (content.includes('URLSession') && content.includes('https') && !hasSSLPinningImplementation) {
|
|
1548
1554
|
if (content.includes('production') || content.includes('release')) {
|
|
1549
1555
|
pushFinding(
|
|
1550
1556
|
"ios.security.missing_ssl_pinning",
|