javi-forge 0.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/.gitignore.template +105 -0
- package/.releaserc +44 -0
- package/README.md +45 -0
- package/ai-config/.skillignore +15 -0
- package/ai-config/AUTO_INVOKE.md +300 -0
- package/ai-config/agents/_TEMPLATE.md +93 -0
- package/ai-config/agents/business/api-designer.md +1657 -0
- package/ai-config/agents/business/business-analyst.md +1331 -0
- package/ai-config/agents/business/product-strategist.md +206 -0
- package/ai-config/agents/business/project-manager.md +178 -0
- package/ai-config/agents/business/requirements-analyst.md +1277 -0
- package/ai-config/agents/business/technical-writer.md +1679 -0
- package/ai-config/agents/creative/ux-designer.md +205 -0
- package/ai-config/agents/data-ai/ai-engineer.md +487 -0
- package/ai-config/agents/data-ai/analytics-engineer.md +953 -0
- package/ai-config/agents/data-ai/data-engineer.md +173 -0
- package/ai-config/agents/data-ai/data-scientist.md +672 -0
- package/ai-config/agents/data-ai/mlops-engineer.md +814 -0
- package/ai-config/agents/data-ai/prompt-engineer.md +772 -0
- package/ai-config/agents/development/angular-expert.md +620 -0
- package/ai-config/agents/development/backend-architect.md +795 -0
- package/ai-config/agents/development/database-specialist.md +212 -0
- package/ai-config/agents/development/frontend-specialist.md +686 -0
- package/ai-config/agents/development/fullstack-engineer.md +668 -0
- package/ai-config/agents/development/golang-pro.md +338 -0
- package/ai-config/agents/development/java-enterprise.md +400 -0
- package/ai-config/agents/development/javascript-pro.md +422 -0
- package/ai-config/agents/development/nextjs-pro.md +474 -0
- package/ai-config/agents/development/python-pro.md +570 -0
- package/ai-config/agents/development/react-pro.md +487 -0
- package/ai-config/agents/development/rust-pro.md +246 -0
- package/ai-config/agents/development/spring-boot-4-expert.md +326 -0
- package/ai-config/agents/development/typescript-pro.md +336 -0
- package/ai-config/agents/development/vue-specialist.md +605 -0
- package/ai-config/agents/infrastructure/cloud-architect.md +472 -0
- package/ai-config/agents/infrastructure/deployment-manager.md +358 -0
- package/ai-config/agents/infrastructure/devops-engineer.md +455 -0
- package/ai-config/agents/infrastructure/incident-responder.md +519 -0
- package/ai-config/agents/infrastructure/kubernetes-expert.md +705 -0
- package/ai-config/agents/infrastructure/monitoring-specialist.md +674 -0
- package/ai-config/agents/infrastructure/performance-engineer.md +658 -0
- package/ai-config/agents/orchestrator.md +241 -0
- package/ai-config/agents/quality/accessibility-auditor.md +1204 -0
- package/ai-config/agents/quality/code-reviewer-compact.md +123 -0
- package/ai-config/agents/quality/code-reviewer.md +363 -0
- package/ai-config/agents/quality/dependency-manager.md +743 -0
- package/ai-config/agents/quality/e2e-test-specialist.md +1005 -0
- package/ai-config/agents/quality/performance-tester.md +1086 -0
- package/ai-config/agents/quality/security-auditor.md +133 -0
- package/ai-config/agents/quality/test-engineer.md +453 -0
- package/ai-config/agents/specialists/api-designer.md +87 -0
- package/ai-config/agents/specialists/backend-architect.md +73 -0
- package/ai-config/agents/specialists/code-reviewer.md +77 -0
- package/ai-config/agents/specialists/db-optimizer.md +75 -0
- package/ai-config/agents/specialists/devops-engineer.md +83 -0
- package/ai-config/agents/specialists/documentation-writer.md +78 -0
- package/ai-config/agents/specialists/frontend-developer.md +75 -0
- package/ai-config/agents/specialists/performance-analyst.md +82 -0
- package/ai-config/agents/specialists/refactor-specialist.md +74 -0
- package/ai-config/agents/specialists/security-auditor.md +74 -0
- package/ai-config/agents/specialists/test-engineer.md +81 -0
- package/ai-config/agents/specialists/ux-consultant.md +76 -0
- package/ai-config/agents/specialized/agent-generator.md +1190 -0
- package/ai-config/agents/specialized/blockchain-developer.md +149 -0
- package/ai-config/agents/specialized/code-migrator.md +892 -0
- package/ai-config/agents/specialized/context-manager.md +978 -0
- package/ai-config/agents/specialized/documentation-writer.md +1078 -0
- package/ai-config/agents/specialized/ecommerce-expert.md +1756 -0
- package/ai-config/agents/specialized/embedded-engineer.md +1714 -0
- package/ai-config/agents/specialized/error-detective.md +1034 -0
- package/ai-config/agents/specialized/fintech-specialist.md +1659 -0
- package/ai-config/agents/specialized/freelance-project-planner-v2.md +1988 -0
- package/ai-config/agents/specialized/freelance-project-planner-v3.md +2136 -0
- package/ai-config/agents/specialized/freelance-project-planner-v4.md +4503 -0
- package/ai-config/agents/specialized/freelance-project-planner.md +722 -0
- package/ai-config/agents/specialized/game-developer.md +1963 -0
- package/ai-config/agents/specialized/healthcare-dev.md +1620 -0
- package/ai-config/agents/specialized/mobile-developer.md +188 -0
- package/ai-config/agents/specialized/parallel-plan-executor.md +506 -0
- package/ai-config/agents/specialized/plan-executor.md +485 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/00-INDEX.md +485 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/01-CORE.md +3493 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/02-SELF-CORRECTION.md +778 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/03-PROGRESSIVE-SETUP.md +918 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/04-DEPLOYMENT.md +1537 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/05-TESTING.md +2633 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/06-OPERATIONS.md +5610 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/INSTALL.md +335 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/QUICK-REFERENCE.txt +215 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/README.md +260 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/START-HERE.md +379 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/WORKFLOW-DIAGRAM.md +355 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/solo-dev-planner.md +279 -0
- package/ai-config/agents/specialized/template-writer.md +347 -0
- package/ai-config/agents/specialized/test-runner.md +99 -0
- package/ai-config/agents/specialized/vibekanban-smart-worker.md +244 -0
- package/ai-config/agents/specialized/wave-executor.md +138 -0
- package/ai-config/agents/specialized/workflow-optimizer.md +1114 -0
- package/ai-config/commands/git/changelog.md +32 -0
- package/ai-config/commands/git/ci-local.md +70 -0
- package/ai-config/commands/git/commit.md +35 -0
- package/ai-config/commands/git/fix-issue.md +23 -0
- package/ai-config/commands/git/pr-create.md +42 -0
- package/ai-config/commands/git/pr-review.md +50 -0
- package/ai-config/commands/git/worktree.md +39 -0
- package/ai-config/commands/refactoring/cleanup.md +24 -0
- package/ai-config/commands/refactoring/dead-code.md +40 -0
- package/ai-config/commands/refactoring/extract.md +31 -0
- package/ai-config/commands/testing/e2e.md +30 -0
- package/ai-config/commands/testing/tdd.md +36 -0
- package/ai-config/commands/testing/test-coverage.md +30 -0
- package/ai-config/commands/testing/test-fix.md +24 -0
- package/ai-config/commands/workflow/generate-agents-md.md +85 -0
- package/ai-config/commands/workflow/planning.md +47 -0
- package/ai-config/commands/workflows/compound.md +89 -0
- package/ai-config/commands/workflows/plan.md +77 -0
- package/ai-config/commands/workflows/review.md +78 -0
- package/ai-config/commands/workflows/work.md +75 -0
- package/ai-config/config.yaml +18 -0
- package/ai-config/hooks/_TEMPLATE.md +96 -0
- package/ai-config/hooks/block-dangerous-commands.md +75 -0
- package/ai-config/hooks/commit-guard.md +90 -0
- package/ai-config/hooks/context-loader.md +73 -0
- package/ai-config/hooks/improve-prompt.md +91 -0
- package/ai-config/hooks/learning-log.md +72 -0
- package/ai-config/hooks/model-router.md +86 -0
- package/ai-config/hooks/secret-scanner.md +64 -0
- package/ai-config/hooks/skill-validator.md +102 -0
- package/ai-config/hooks/task-artifact.md +114 -0
- package/ai-config/hooks/validate-workflow.md +100 -0
- package/ai-config/prompts/base.md +71 -0
- package/ai-config/prompts/modes/debug.md +34 -0
- package/ai-config/prompts/modes/deploy.md +40 -0
- package/ai-config/prompts/modes/research.md +32 -0
- package/ai-config/prompts/modes/review.md +33 -0
- package/ai-config/prompts/review-policy.md +79 -0
- package/ai-config/skills/_TEMPLATE.md +157 -0
- package/ai-config/skills/backend/api-gateway/SKILL.md +254 -0
- package/ai-config/skills/backend/bff-concepts/SKILL.md +239 -0
- package/ai-config/skills/backend/bff-spring/SKILL.md +364 -0
- package/ai-config/skills/backend/chi-router/SKILL.md +396 -0
- package/ai-config/skills/backend/error-handling/SKILL.md +255 -0
- package/ai-config/skills/backend/exceptions-spring/SKILL.md +323 -0
- package/ai-config/skills/backend/fastapi/SKILL.md +302 -0
- package/ai-config/skills/backend/gateway-spring/SKILL.md +390 -0
- package/ai-config/skills/backend/go-backend/SKILL.md +457 -0
- package/ai-config/skills/backend/gradle-multimodule/SKILL.md +274 -0
- package/ai-config/skills/backend/graphql-concepts/SKILL.md +352 -0
- package/ai-config/skills/backend/graphql-spring/SKILL.md +398 -0
- package/ai-config/skills/backend/grpc-concepts/SKILL.md +283 -0
- package/ai-config/skills/backend/grpc-spring/SKILL.md +445 -0
- package/ai-config/skills/backend/jwt-auth/SKILL.md +412 -0
- package/ai-config/skills/backend/notifications-concepts/SKILL.md +259 -0
- package/ai-config/skills/backend/recommendations-concepts/SKILL.md +261 -0
- package/ai-config/skills/backend/search-concepts/SKILL.md +263 -0
- package/ai-config/skills/backend/search-spring/SKILL.md +375 -0
- package/ai-config/skills/backend/spring-boot-4/SKILL.md +172 -0
- package/ai-config/skills/backend/websockets/SKILL.md +532 -0
- package/ai-config/skills/data-ai/ai-ml/SKILL.md +423 -0
- package/ai-config/skills/data-ai/analytics-concepts/SKILL.md +195 -0
- package/ai-config/skills/data-ai/analytics-spring/SKILL.md +340 -0
- package/ai-config/skills/data-ai/duckdb-analytics/SKILL.md +440 -0
- package/ai-config/skills/data-ai/langchain/SKILL.md +238 -0
- package/ai-config/skills/data-ai/mlflow/SKILL.md +302 -0
- package/ai-config/skills/data-ai/onnx-inference/SKILL.md +290 -0
- package/ai-config/skills/data-ai/powerbi/SKILL.md +352 -0
- package/ai-config/skills/data-ai/pytorch/SKILL.md +274 -0
- package/ai-config/skills/data-ai/scikit-learn/SKILL.md +321 -0
- package/ai-config/skills/data-ai/vector-db/SKILL.md +301 -0
- package/ai-config/skills/database/graph-databases/SKILL.md +218 -0
- package/ai-config/skills/database/graph-spring/SKILL.md +361 -0
- package/ai-config/skills/database/pgx-postgres/SKILL.md +512 -0
- package/ai-config/skills/database/redis-cache/SKILL.md +343 -0
- package/ai-config/skills/database/sqlite-embedded/SKILL.md +388 -0
- package/ai-config/skills/database/timescaledb/SKILL.md +320 -0
- package/ai-config/skills/docs/api-documentation/SKILL.md +293 -0
- package/ai-config/skills/docs/docs-spring/SKILL.md +377 -0
- package/ai-config/skills/docs/mustache-templates/SKILL.md +190 -0
- package/ai-config/skills/docs/technical-docs/SKILL.md +447 -0
- package/ai-config/skills/frontend/astro-ssr/SKILL.md +441 -0
- package/ai-config/skills/frontend/frontend-design/SKILL.md +54 -0
- package/ai-config/skills/frontend/frontend-web/SKILL.md +368 -0
- package/ai-config/skills/frontend/mantine-ui/SKILL.md +396 -0
- package/ai-config/skills/frontend/tanstack-query/SKILL.md +439 -0
- package/ai-config/skills/frontend/zod-validation/SKILL.md +417 -0
- package/ai-config/skills/frontend/zustand-state/SKILL.md +350 -0
- package/ai-config/skills/infrastructure/chaos-engineering/SKILL.md +244 -0
- package/ai-config/skills/infrastructure/chaos-spring/SKILL.md +378 -0
- package/ai-config/skills/infrastructure/devops-infra/SKILL.md +435 -0
- package/ai-config/skills/infrastructure/docker-containers/SKILL.md +420 -0
- package/ai-config/skills/infrastructure/kubernetes/SKILL.md +456 -0
- package/ai-config/skills/infrastructure/opentelemetry/SKILL.md +546 -0
- package/ai-config/skills/infrastructure/traefik-proxy/SKILL.md +474 -0
- package/ai-config/skills/infrastructure/woodpecker-ci/SKILL.md +315 -0
- package/ai-config/skills/mobile/ionic-capacitor/SKILL.md +504 -0
- package/ai-config/skills/mobile/mobile-ionic/SKILL.md +448 -0
- package/ai-config/skills/prompt-improver/SKILL.md +125 -0
- package/ai-config/skills/quality/ghagga-review/SKILL.md +216 -0
- package/ai-config/skills/references/hooks-patterns/SKILL.md +238 -0
- package/ai-config/skills/references/mcp-servers/SKILL.md +275 -0
- package/ai-config/skills/references/plugins-reference/SKILL.md +110 -0
- package/ai-config/skills/references/skills-reference/SKILL.md +420 -0
- package/ai-config/skills/references/subagent-templates/SKILL.md +193 -0
- package/ai-config/skills/systems-iot/modbus-protocol/SKILL.md +410 -0
- package/ai-config/skills/systems-iot/mqtt-rumqttc/SKILL.md +408 -0
- package/ai-config/skills/systems-iot/rust-systems/SKILL.md +386 -0
- package/ai-config/skills/systems-iot/tokio-async/SKILL.md +324 -0
- package/ai-config/skills/testing/playwright-e2e/SKILL.md +289 -0
- package/ai-config/skills/testing/testcontainers/SKILL.md +299 -0
- package/ai-config/skills/testing/vitest-testing/SKILL.md +381 -0
- package/ai-config/skills/workflow/ci-local-guide/SKILL.md +118 -0
- package/ai-config/skills/workflow/claude-automation-recommender/SKILL.md +299 -0
- package/ai-config/skills/workflow/claude-md-improver/SKILL.md +158 -0
- package/ai-config/skills/workflow/finishing-a-development-branch/SKILL.md +117 -0
- package/ai-config/skills/workflow/git-github/SKILL.md +334 -0
- package/ai-config/skills/workflow/git-github/references/examples.md +160 -0
- package/ai-config/skills/workflow/git-workflow/SKILL.md +214 -0
- package/ai-config/skills/workflow/ide-plugins/SKILL.md +277 -0
- package/ai-config/skills/workflow/ide-plugins-intellij/SKILL.md +401 -0
- package/ai-config/skills/workflow/obsidian-brain-workflow/SKILL.md +199 -0
- package/ai-config/skills/workflow/using-git-worktrees/SKILL.md +100 -0
- package/ai-config/skills/workflow/verification-before-completion/SKILL.md +73 -0
- package/ai-config/skills/workflow/wave-workflow/SKILL.md +178 -0
- package/ci-local/README.md +170 -0
- package/ci-local/ci-local.sh +297 -0
- package/ci-local/hooks/commit-msg +74 -0
- package/ci-local/hooks/pre-commit +162 -0
- package/ci-local/hooks/pre-push +41 -0
- package/ci-local/install.sh +49 -0
- package/ci-local/semgrep.yml +214 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +55 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/analyze.test.d.ts +2 -0
- package/dist/commands/analyze.test.d.ts.map +1 -0
- package/dist/commands/analyze.test.js +145 -0
- package/dist/commands/analyze.test.js.map +1 -0
- package/dist/commands/doctor.d.ts +7 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +158 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/doctor.test.d.ts +2 -0
- package/dist/commands/doctor.test.d.ts.map +1 -0
- package/dist/commands/doctor.test.js +200 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +283 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +2 -0
- package/dist/commands/init.test.d.ts.map +1 -0
- package/dist/commands/init.test.js +271 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/sync.d.ts +8 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +201 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +57 -0
- package/dist/constants.js.map +1 -0
- package/dist/e2e/aggressive.e2e.test.d.ts +2 -0
- package/dist/e2e/aggressive.e2e.test.d.ts.map +1 -0
- package/dist/e2e/aggressive.e2e.test.js +350 -0
- package/dist/e2e/aggressive.e2e.test.js.map +1 -0
- package/dist/e2e/commands.e2e.test.d.ts +2 -0
- package/dist/e2e/commands.e2e.test.d.ts.map +1 -0
- package/dist/e2e/commands.e2e.test.js +213 -0
- package/dist/e2e/commands.e2e.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/common.d.ts +17 -0
- package/dist/lib/common.d.ts.map +1 -0
- package/dist/lib/common.js +111 -0
- package/dist/lib/common.js.map +1 -0
- package/dist/lib/common.test.d.ts +2 -0
- package/dist/lib/common.test.d.ts.map +1 -0
- package/dist/lib/common.test.js +316 -0
- package/dist/lib/common.test.js.map +1 -0
- package/dist/lib/frontmatter.d.ts +18 -0
- package/dist/lib/frontmatter.d.ts.map +1 -0
- package/dist/lib/frontmatter.js +61 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/frontmatter.test.d.ts +2 -0
- package/dist/lib/frontmatter.test.d.ts.map +1 -0
- package/dist/lib/frontmatter.test.js +257 -0
- package/dist/lib/frontmatter.test.js.map +1 -0
- package/dist/lib/template.d.ts +24 -0
- package/dist/lib/template.d.ts.map +1 -0
- package/dist/lib/template.js +78 -0
- package/dist/lib/template.js.map +1 -0
- package/dist/lib/template.test.d.ts +2 -0
- package/dist/lib/template.test.d.ts.map +1 -0
- package/dist/lib/template.test.js +201 -0
- package/dist/lib/template.test.js.map +1 -0
- package/dist/types/index.d.ts +48 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/AnalyzeUI.d.ts +7 -0
- package/dist/ui/AnalyzeUI.d.ts.map +1 -0
- package/dist/ui/AnalyzeUI.js +100 -0
- package/dist/ui/AnalyzeUI.js.map +1 -0
- package/dist/ui/App.d.ts +13 -0
- package/dist/ui/App.d.ts.map +1 -0
- package/dist/ui/App.js +100 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/CIContext.d.ts +9 -0
- package/dist/ui/CIContext.d.ts.map +1 -0
- package/dist/ui/CIContext.js +9 -0
- package/dist/ui/CIContext.js.map +1 -0
- package/dist/ui/CISelector.d.ts +8 -0
- package/dist/ui/CISelector.d.ts.map +1 -0
- package/dist/ui/CISelector.js +45 -0
- package/dist/ui/CISelector.js.map +1 -0
- package/dist/ui/Doctor.d.ts +3 -0
- package/dist/ui/Doctor.d.ts.map +1 -0
- package/dist/ui/Doctor.js +89 -0
- package/dist/ui/Doctor.js.map +1 -0
- package/dist/ui/Header.d.ts +8 -0
- package/dist/ui/Header.d.ts.map +1 -0
- package/dist/ui/Header.js +30 -0
- package/dist/ui/Header.js.map +1 -0
- package/dist/ui/MemorySelector.d.ts +8 -0
- package/dist/ui/MemorySelector.d.ts.map +1 -0
- package/dist/ui/MemorySelector.js +46 -0
- package/dist/ui/MemorySelector.js.map +1 -0
- package/dist/ui/NameInput.d.ts +8 -0
- package/dist/ui/NameInput.d.ts.map +1 -0
- package/dist/ui/NameInput.js +69 -0
- package/dist/ui/NameInput.js.map +1 -0
- package/dist/ui/OptionSelector.d.ts +12 -0
- package/dist/ui/OptionSelector.d.ts.map +1 -0
- package/dist/ui/OptionSelector.js +69 -0
- package/dist/ui/OptionSelector.js.map +1 -0
- package/dist/ui/Progress.d.ts +11 -0
- package/dist/ui/Progress.d.ts.map +1 -0
- package/dist/ui/Progress.js +58 -0
- package/dist/ui/Progress.js.map +1 -0
- package/dist/ui/StackSelector.d.ts +9 -0
- package/dist/ui/StackSelector.d.ts.map +1 -0
- package/dist/ui/StackSelector.js +65 -0
- package/dist/ui/StackSelector.js.map +1 -0
- package/dist/ui/Summary.d.ts +12 -0
- package/dist/ui/Summary.d.ts.map +1 -0
- package/dist/ui/Summary.js +114 -0
- package/dist/ui/Summary.js.map +1 -0
- package/dist/ui/SyncUI.d.ts +10 -0
- package/dist/ui/SyncUI.d.ts.map +1 -0
- package/dist/ui/SyncUI.js +64 -0
- package/dist/ui/SyncUI.js.map +1 -0
- package/dist/ui/Welcome.d.ts +7 -0
- package/dist/ui/Welcome.d.ts.map +1 -0
- package/dist/ui/Welcome.js +45 -0
- package/dist/ui/Welcome.js.map +1 -0
- package/dist/ui/theme.d.ts +10 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +9 -0
- package/dist/ui/theme.js.map +1 -0
- package/modules/engram/.gitignore-snippet.txt +6 -0
- package/modules/engram/.mcp-config-snippet.json +11 -0
- package/modules/engram/README.md +146 -0
- package/modules/engram/install-engram.sh +216 -0
- package/modules/ghagga/.env.example +43 -0
- package/modules/ghagga/README.md +153 -0
- package/modules/ghagga/docker-compose.yml +80 -0
- package/modules/ghagga/setup-ghagga.sh +139 -0
- package/modules/memory-simple/.project/NOTES.md +22 -0
- package/modules/memory-simple/README.md +23 -0
- package/modules/obsidian-brain/.obsidian/app.json +23 -0
- package/modules/obsidian-brain/.obsidian/appearance.json +5 -0
- package/modules/obsidian-brain/.obsidian/bookmarks.json +34 -0
- package/modules/obsidian-brain/.obsidian/community-plugins.json +1 -0
- package/modules/obsidian-brain/.obsidian/core-plugins-migration.json +21 -0
- package/modules/obsidian-brain/.obsidian/core-plugins.json +18 -0
- package/modules/obsidian-brain/.obsidian/daily-notes.json +5 -0
- package/modules/obsidian-brain/.obsidian/graph.json +37 -0
- package/modules/obsidian-brain/.obsidian/hotkeys.json +14 -0
- package/modules/obsidian-brain/.obsidian/plugins/dataview/data.json +25 -0
- package/modules/obsidian-brain/.obsidian/plugins/obsidian-kanban/data.json +29 -0
- package/modules/obsidian-brain/.obsidian/plugins/templater-obsidian/data.json +18 -0
- package/modules/obsidian-brain/.obsidian/snippets/project-memory.css +71 -0
- package/modules/obsidian-brain/.obsidian-gitignore-snippet.txt +8 -0
- package/modules/obsidian-brain/.project/Attachments/.gitkeep +0 -0
- package/modules/obsidian-brain/.project/Memory/BLOCKERS.md +78 -0
- package/modules/obsidian-brain/.project/Memory/CONTEXT.md +102 -0
- package/modules/obsidian-brain/.project/Memory/DASHBOARD.md +73 -0
- package/modules/obsidian-brain/.project/Memory/DECISIONS.md +87 -0
- package/modules/obsidian-brain/.project/Memory/KANBAN.md +15 -0
- package/modules/obsidian-brain/.project/Memory/README.md +61 -0
- package/modules/obsidian-brain/.project/Memory/WAVES.md +78 -0
- package/modules/obsidian-brain/.project/Sessions/TEMPLATE.md +99 -0
- package/modules/obsidian-brain/.project/Templates/ADR.md +33 -0
- package/modules/obsidian-brain/.project/Templates/Blocker.md +21 -0
- package/modules/obsidian-brain/.project/Templates/Session.md +88 -0
- package/modules/obsidian-brain/README.md +268 -0
- package/modules/obsidian-brain/new-wave.sh +182 -0
- package/package.json +51 -0
- package/schemas/agent.schema.json +34 -0
- package/schemas/ai-config.schema.json +28 -0
- package/schemas/skill.schema.json +44 -0
- package/src/commands/analyze.test.ts +145 -0
- package/src/commands/analyze.ts +69 -0
- package/src/commands/doctor.test.ts +208 -0
- package/src/commands/doctor.ts +163 -0
- package/src/commands/init.test.ts +298 -0
- package/src/commands/init.ts +285 -0
- package/src/constants.ts +69 -0
- package/src/e2e/aggressive.e2e.test.ts +557 -0
- package/src/e2e/commands.e2e.test.ts +298 -0
- package/src/index.tsx +106 -0
- package/src/lib/common.test.ts +318 -0
- package/src/lib/common.ts +127 -0
- package/src/lib/frontmatter.test.ts +291 -0
- package/src/lib/frontmatter.ts +77 -0
- package/src/lib/template.test.ts +226 -0
- package/src/lib/template.ts +99 -0
- package/src/types/index.ts +53 -0
- package/src/ui/AnalyzeUI.tsx +133 -0
- package/src/ui/App.tsx +175 -0
- package/src/ui/CIContext.tsx +25 -0
- package/src/ui/CISelector.tsx +72 -0
- package/src/ui/Doctor.tsx +122 -0
- package/src/ui/Header.tsx +48 -0
- package/src/ui/MemorySelector.tsx +73 -0
- package/src/ui/NameInput.tsx +82 -0
- package/src/ui/OptionSelector.tsx +100 -0
- package/src/ui/Progress.tsx +88 -0
- package/src/ui/StackSelector.tsx +101 -0
- package/src/ui/Summary.tsx +134 -0
- package/src/ui/Welcome.tsx +54 -0
- package/src/ui/theme.ts +10 -0
- package/stryker.config.json +19 -0
- package/tasks/_TEMPLATE/files-edited.md +3 -0
- package/tasks/_TEMPLATE/plan.md +3 -0
- package/tasks/_TEMPLATE/research.md +3 -0
- package/tasks/_TEMPLATE/verification.md +5 -0
- package/templates/common/dependabot/cargo.yml +11 -0
- package/templates/common/dependabot/github-actions.yml +16 -0
- package/templates/common/dependabot/gomod.yml +15 -0
- package/templates/common/dependabot/gradle.yml +15 -0
- package/templates/common/dependabot/header.yml +3 -0
- package/templates/common/dependabot/maven.yml +15 -0
- package/templates/common/dependabot/npm.yml +20 -0
- package/templates/common/dependabot/pip.yml +11 -0
- package/templates/dependabot.yml +162 -0
- package/templates/github/ci-go.yml +41 -0
- package/templates/github/ci-java.yml +45 -0
- package/templates/github/ci-monorepo.yml +150 -0
- package/templates/github/ci-node.yml +42 -0
- package/templates/github/ci-python.yml +42 -0
- package/templates/github/ci-rust.yml +42 -0
- package/templates/github/dependabot-automerge.yml +40 -0
- package/templates/gitlab/gitlab-ci-go.yml +88 -0
- package/templates/gitlab/gitlab-ci-java.yml +79 -0
- package/templates/gitlab/gitlab-ci-monorepo.yml +126 -0
- package/templates/gitlab/gitlab-ci-node.yml +63 -0
- package/templates/gitlab/gitlab-ci-python.yml +147 -0
- package/templates/gitlab/gitlab-ci-rust.yml +67 -0
- package/templates/global/claude-settings.json +98 -0
- package/templates/global/codex-config.toml +8 -0
- package/templates/global/copilot-instructions/base-rules.instructions.md +13 -0
- package/templates/global/copilot-instructions/sdd-orchestrator.instructions.md +37 -0
- package/templates/global/gemini-commands/cleanup.toml +20 -0
- package/templates/global/gemini-commands/commit.toml +15 -0
- package/templates/global/gemini-commands/dead-code.toml +22 -0
- package/templates/global/gemini-commands/plan.toml +30 -0
- package/templates/global/gemini-commands/review.toml +17 -0
- package/templates/global/gemini-commands/sdd-apply.toml +22 -0
- package/templates/global/gemini-commands/sdd-ff.toml +14 -0
- package/templates/global/gemini-commands/sdd-new.toml +21 -0
- package/templates/global/gemini-commands/sdd-verify.toml +21 -0
- package/templates/global/gemini-commands/tdd.toml +26 -0
- package/templates/global/gemini-settings.json +8 -0
- package/templates/global/opencode-config.json +44 -0
- package/templates/global/sdd-instructions.md +47 -0
- package/templates/global/sdd-orchestrator-claude.md +46 -0
- package/templates/global/sdd-orchestrator-copilot.md +34 -0
- package/templates/renovate.json +69 -0
- package/templates/woodpecker/monorepo/backend.yml +34 -0
- package/templates/woodpecker/monorepo/frontend.yml +34 -0
- package/templates/woodpecker/monorepo/summary.yml +25 -0
- package/templates/woodpecker/woodpecker-go.yml +51 -0
- package/templates/woodpecker/woodpecker-java.yml +67 -0
- package/templates/woodpecker/woodpecker-node.yml +47 -0
- package/templates/woodpecker/woodpecker-python.yml +108 -0
- package/templates/woodpecker/woodpecker-rust.yml +57 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +16 -0
- package/workflows/reusable-build-go.yml +111 -0
- package/workflows/reusable-build-java.yml +120 -0
- package/workflows/reusable-build-node.yml +145 -0
- package/workflows/reusable-build-python.yml +159 -0
- package/workflows/reusable-build-rust.yml +135 -0
- package/workflows/reusable-docker.yml +120 -0
- package/workflows/reusable-ghagga-review.yml +165 -0
- package/workflows/reusable-release.yml +91 -0
|
@@ -0,0 +1,4503 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: freelance-project-planner-v4
|
|
3
|
+
description: Agente completo para planificación freelance multi-lenguaje con GitFlow, Stacked PRs, y Preview Environments
|
|
4
|
+
trigger: >
|
|
5
|
+
freelance v4, GitFlow, stacked PRs, preview environments, merge queue, CODEOWNERS,
|
|
6
|
+
multi-language planning, auto-sync, Slack Discord integration, changelog
|
|
7
|
+
category: specialized
|
|
8
|
+
color: green
|
|
9
|
+
tools: Write, Read, MultiEdit, Bash, Grep, Glob
|
|
10
|
+
config:
|
|
11
|
+
model: opus
|
|
12
|
+
metadata:
|
|
13
|
+
version: "2.0"
|
|
14
|
+
updated: "2026-02"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 🎯 Filosofía Core: GitFlow + Stacked PRs + Infrastructure First
|
|
18
|
+
|
|
19
|
+
Este agente combina las mejores prácticas de:
|
|
20
|
+
- **GitFlow** - Estrategia de branching profesional
|
|
21
|
+
- **Stacked PRs** - PRs pequeños y apilados para review incremental
|
|
22
|
+
- **Infrastructure First** - Docker y CI/CD como prioridad #1
|
|
23
|
+
- **Aprendizaje Progresivo** - Cada tarea enseña conceptos cuando los necesitas
|
|
24
|
+
- **Kanban Light + XP Adaptado** - Metodología ágil optimizada para freelancers
|
|
25
|
+
|
|
26
|
+
### Orden de Prioridades
|
|
27
|
+
```
|
|
28
|
+
1️⃣ Dockerización completa (dev + prod)
|
|
29
|
+
2️⃣ GitHub Actions (CI/CD con soporte para stacks)
|
|
30
|
+
3️⃣ GitFlow setup (branches + protections)
|
|
31
|
+
4️⃣ Issues organizados en Stacked PRs
|
|
32
|
+
5️⃣ Desarrollo incremental con review continuo
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 📋 Metodología: Kanban Light + XP Adaptado (Heredado de v1)
|
|
38
|
+
|
|
39
|
+
### Framework Híbrido para Freelancers
|
|
40
|
+
|
|
41
|
+
Este agente hereda la metodología probada de v1, optimizada para desarrolladores freelance:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const KANBAN_XP_CONFIG = {
|
|
45
|
+
// KANBAN LIGHT
|
|
46
|
+
kanban: {
|
|
47
|
+
// Tablero simplificado
|
|
48
|
+
columns: ['Backlog', 'Ready', 'In Progress', 'Review', 'Done'],
|
|
49
|
+
|
|
50
|
+
// WIP Limits - CRÍTICO para freelancers
|
|
51
|
+
wipLimits: {
|
|
52
|
+
ready: 5,
|
|
53
|
+
inProgress: 2, // Máximo 2 tareas activas
|
|
54
|
+
review: 3
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Carriles de prioridad
|
|
58
|
+
priorityLanes: [
|
|
59
|
+
{ name: '🔥 Crítico', color: 'red', sla: '24h' },
|
|
60
|
+
{ name: '⚡ Alta', color: 'orange', sla: '3d' },
|
|
61
|
+
{ name: '📝 Normal', color: 'blue', sla: '1w' },
|
|
62
|
+
{ name: '🔧 Técnico', color: 'gray', sla: '2w' }
|
|
63
|
+
],
|
|
64
|
+
|
|
65
|
+
// Sin ceremonias innecesarias
|
|
66
|
+
ceremonies: {
|
|
67
|
+
dailyStandup: false, // No necesario para solo dev
|
|
68
|
+
weeklyPlanning: true, // Lunes: revisar backlog
|
|
69
|
+
weeklyDemo: true, // Viernes: demo al cliente
|
|
70
|
+
retrospective: 'monthly' // Una vez al mes
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// XP ADAPTADO
|
|
75
|
+
xp: {
|
|
76
|
+
// TDD Selectivo - Solo en áreas críticas
|
|
77
|
+
tdd: {
|
|
78
|
+
enabled: true,
|
|
79
|
+
scope: 'critical-only',
|
|
80
|
+
criticalAreas: [
|
|
81
|
+
'payment-processing',
|
|
82
|
+
'authentication',
|
|
83
|
+
'data-validation',
|
|
84
|
+
'public-apis',
|
|
85
|
+
'business-logic-core'
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// CI/CD Obligatorio
|
|
90
|
+
continuousIntegration: {
|
|
91
|
+
required: true,
|
|
92
|
+
onEveryPR: true,
|
|
93
|
+
testsRequired: ['unit', 'lint', 'type-check'],
|
|
94
|
+
optionalTests: ['integration', 'e2e']
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Refactorización Planificada
|
|
98
|
+
refactoring: {
|
|
99
|
+
schedule: 'friday-after-demo',
|
|
100
|
+
maxTimePerWeek: '2h',
|
|
101
|
+
triggerThreshold: {
|
|
102
|
+
cyclomaticComplexity: 10,
|
|
103
|
+
duplicateCode: 0.8
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// Pair Programming (adaptado)
|
|
108
|
+
pairProgramming: {
|
|
109
|
+
mode: 'rubber-duck', // Hablar con uno mismo o el pato
|
|
110
|
+
codeReview: 'self-review-24h', // Auto-review después de 24h
|
|
111
|
+
aiAssisted: true // Usar Claude como pair
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// Diseño Simple
|
|
115
|
+
simpleDesign: {
|
|
116
|
+
yagni: true, // No construir lo que no necesitas
|
|
117
|
+
kiss: true, // Mantener simple
|
|
118
|
+
dryThreshold: 3 // Extraer después de 3 repeticiones
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Tablero Kanban con Stacked PRs
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
128
|
+
│ 📋 KANBAN BOARD - Proyecto X │
|
|
129
|
+
├─────────────┬─────────────┬─────────────┬─────────────┬─────────────────────┤
|
|
130
|
+
│ 📥 BACKLOG │ ✅ READY │ 🔨 IN PROG │ 👀 REVIEW │ ✅ DONE │
|
|
131
|
+
│ │ (max 5) │ (max 2) │ (max 3) │ │
|
|
132
|
+
├─────────────┼─────────────┼─────────────┼─────────────┼─────────────────────┤
|
|
133
|
+
│ │ │ │ │ │
|
|
134
|
+
│ 🔥 CRÍTICO │ │ │ │ │
|
|
135
|
+
│ ─────────── │ │ ┌─────────┐ │ │ │
|
|
136
|
+
│ │ │ │Stack #1 │ │ │ │
|
|
137
|
+
│ │ │ │PR 02/04 │ │ │ │
|
|
138
|
+
│ │ │ │Auth API │ │ │ │
|
|
139
|
+
│ │ │ └─────────┘ │ │ │
|
|
140
|
+
├─────────────┼─────────────┼─────────────┼─────────────┼─────────────────────┤
|
|
141
|
+
│ ⚡ ALTA │ │ │ │ │
|
|
142
|
+
│ ─────────── │ ┌─────────┐ │ ┌─────────┐ │ ┌─────────┐ │ ┌─────────┐ │
|
|
143
|
+
│ ┌─────────┐ │ │Stack #2 │ │ │Stack #1 │ │ │Stack #1 │ │ │Stack #1 │ │
|
|
144
|
+
│ │Feature │ │ │PR 01/03 │ │ │PR 03/04 │ │ │PR 01/04 │ │ │PR 01/04 │ │
|
|
145
|
+
│ │Payment │ │ │Stripe │ │ │JWT Mid │ │ │DB Schema│ │ │Complete │ │
|
|
146
|
+
│ └─────────┘ │ └─────────┘ │ └─────────┘ │ └─────────┘ │ └─────────┘ │
|
|
147
|
+
├─────────────┼─────────────┼─────────────┼─────────────┼─────────────────────┤
|
|
148
|
+
│ 📝 NORMAL │ │ │ │ │
|
|
149
|
+
│ ─────────── │ ┌─────────┐ │ │ │ │
|
|
150
|
+
│ ┌─────────┐ │ │Bug fix │ │ │ │ │
|
|
151
|
+
│ │Feature │ │ │#234 │ │ │ │ │
|
|
152
|
+
│ │Reports │ │ └─────────┘ │ │ │ │
|
|
153
|
+
│ └─────────┘ │ │ │ │ │
|
|
154
|
+
├─────────────┼─────────────┼─────────────┼─────────────┼─────────────────────┤
|
|
155
|
+
│ 🔧 TÉCNICO │ │ │ │ │
|
|
156
|
+
│ ─────────── │ ┌─────────┐ │ │ │ │
|
|
157
|
+
│ ┌─────────┐ │ │Refactor │ │ │ │ │
|
|
158
|
+
│ │Tech Debt│ │ │Utils │ │ │ │ │
|
|
159
|
+
│ │Cleanup │ │ └─────────┘ │ │ │ │
|
|
160
|
+
│ └─────────┘ │ │ │ │ │
|
|
161
|
+
└─────────────┴─────────────┴─────────────┴─────────────┴─────────────────────┘
|
|
162
|
+
|
|
163
|
+
📊 WIP Status: 2/2 (límite alcanzado)
|
|
164
|
+
📈 Velocity: 8 PRs/semana
|
|
165
|
+
⏱️ Avg Cycle Time: 1.5 días
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### TDD Selectivo en Stacked PRs
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
class SelectiveTDDStrategy {
|
|
172
|
+
/**
|
|
173
|
+
* Determina si un PR del stack requiere TDD
|
|
174
|
+
*/
|
|
175
|
+
shouldUseTDD(pr: StackedPR): boolean {
|
|
176
|
+
// TDD obligatorio en áreas críticas
|
|
177
|
+
const criticalPatterns = [
|
|
178
|
+
/payment/i,
|
|
179
|
+
/auth/i,
|
|
180
|
+
/security/i,
|
|
181
|
+
/validation/i,
|
|
182
|
+
/api.*endpoint/i
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const isCritical = criticalPatterns.some(pattern =>
|
|
186
|
+
pattern.test(pr.title) || pattern.test(pr.branch)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// TDD recomendado si tiene lógica de negocio compleja
|
|
190
|
+
const hasComplexLogic = pr.estimatedLines > 200 ||
|
|
191
|
+
pr.conceptsIntroduced.length > 2;
|
|
192
|
+
|
|
193
|
+
return isCritical || hasComplexLogic;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Genera guía de TDD para el PR
|
|
198
|
+
*/
|
|
199
|
+
generateTDDGuide(pr: StackedPR): string {
|
|
200
|
+
if (!this.shouldUseTDD(pr)) {
|
|
201
|
+
return `
|
|
202
|
+
### 🧪 Testing para este PR
|
|
203
|
+
|
|
204
|
+
Este PR no requiere TDD estricto, pero sí:
|
|
205
|
+
- [ ] Tests unitarios básicos
|
|
206
|
+
- [ ] Verificación manual
|
|
207
|
+
|
|
208
|
+
**Razón**: No es área crítica del sistema.
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return `
|
|
213
|
+
### 🧪 TDD Requerido para este PR
|
|
214
|
+
|
|
215
|
+
Este PR está en un **área crítica** y requiere Test-Driven Development:
|
|
216
|
+
|
|
217
|
+
#### Ciclo TDD
|
|
218
|
+
\`\`\`
|
|
219
|
+
1. 🔴 RED: Escribir test que falla
|
|
220
|
+
2. 🟢 GREEN: Código mínimo para pasar
|
|
221
|
+
3. 🔵 REFACTOR: Mejorar sin romper tests
|
|
222
|
+
\`\`\`
|
|
223
|
+
|
|
224
|
+
#### Tests Requeridos
|
|
225
|
+
${this.generateRequiredTests(pr)}
|
|
226
|
+
|
|
227
|
+
#### Cobertura Mínima
|
|
228
|
+
- Líneas: 80%
|
|
229
|
+
- Branches: 75%
|
|
230
|
+
- Functions: 90%
|
|
231
|
+
|
|
232
|
+
**⚠️ El PR no será aprobado sin tests adecuados.**
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### CI/CD con Soporte Kanban + Stacks
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
# .github/workflows/kanban-ci.yml
|
|
242
|
+
name: Kanban + Stacked PR CI
|
|
243
|
+
|
|
244
|
+
on:
|
|
245
|
+
pull_request:
|
|
246
|
+
types: [opened, synchronize, reopened, labeled]
|
|
247
|
+
|
|
248
|
+
jobs:
|
|
249
|
+
# Verificar WIP limits
|
|
250
|
+
check-wip:
|
|
251
|
+
runs-on: ubuntu-latest
|
|
252
|
+
steps:
|
|
253
|
+
- name: Check WIP Limits
|
|
254
|
+
uses: actions/github-script@v7
|
|
255
|
+
with:
|
|
256
|
+
script: |
|
|
257
|
+
// Contar PRs "in progress" del autor
|
|
258
|
+
const author = context.payload.pull_request.user.login;
|
|
259
|
+
const { data: prs } = await github.rest.pulls.list({
|
|
260
|
+
owner: context.repo.owner,
|
|
261
|
+
repo: context.repo.repo,
|
|
262
|
+
state: 'open',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const inProgressPRs = prs.filter(pr =>
|
|
266
|
+
pr.user.login === author &&
|
|
267
|
+
pr.labels.some(l => l.name === 'in-progress')
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const WIP_LIMIT = 2;
|
|
271
|
+
|
|
272
|
+
if (inProgressPRs.length >= WIP_LIMIT) {
|
|
273
|
+
core.warning(`⚠️ WIP Limit alcanzado (${inProgressPRs.length}/${WIP_LIMIT})`);
|
|
274
|
+
core.warning('Completa PRs existentes antes de iniciar nuevos');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# Tests según criticidad
|
|
278
|
+
selective-tests:
|
|
279
|
+
runs-on: ubuntu-latest
|
|
280
|
+
outputs:
|
|
281
|
+
needs_tdd: ${{ steps.check.outputs.needs_tdd }}
|
|
282
|
+
steps:
|
|
283
|
+
- uses: actions/checkout@v4
|
|
284
|
+
|
|
285
|
+
- name: Detect Critical Area
|
|
286
|
+
id: check
|
|
287
|
+
run: |
|
|
288
|
+
FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
|
289
|
+
|
|
290
|
+
# Patrones críticos que requieren TDD
|
|
291
|
+
if echo "$FILES" | grep -qE "(payment|auth|security|validation)"; then
|
|
292
|
+
echo "needs_tdd=true" >> $GITHUB_OUTPUT
|
|
293
|
+
echo "🔴 Área crítica detectada - TDD requerido"
|
|
294
|
+
else
|
|
295
|
+
echo "needs_tdd=false" >> $GITHUB_OUTPUT
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
- name: Run Standard Tests
|
|
299
|
+
run: |
|
|
300
|
+
npm ci
|
|
301
|
+
npm run lint
|
|
302
|
+
npm run type-check
|
|
303
|
+
npm run test:unit
|
|
304
|
+
|
|
305
|
+
- name: Run Extended Tests (Critical Areas)
|
|
306
|
+
if: steps.check.outputs.needs_tdd == 'true'
|
|
307
|
+
run: |
|
|
308
|
+
npm run test:integration
|
|
309
|
+
npm run test:coverage -- --threshold 80
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Refactorización Continua Planificada
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
class RefactoringScheduler {
|
|
316
|
+
/**
|
|
317
|
+
* Programa tareas de refactorización según métricas
|
|
318
|
+
*/
|
|
319
|
+
async scheduleRefactoring(
|
|
320
|
+
analysis: ProjectAnalysis
|
|
321
|
+
): Promise<RefactoringTask[]> {
|
|
322
|
+
const tasks: RefactoringTask[] = [];
|
|
323
|
+
|
|
324
|
+
// Análisis de complejidad ciclomática
|
|
325
|
+
for (const module of analysis.modules) {
|
|
326
|
+
if (module.cyclomaticComplexity > 10) {
|
|
327
|
+
tasks.push({
|
|
328
|
+
type: 'complexity',
|
|
329
|
+
title: `🔧 Simplificar: ${module.name}`,
|
|
330
|
+
priority: this.calculatePriority(module.cyclomaticComplexity),
|
|
331
|
+
estimate: `${Math.ceil(module.cyclomaticComplexity / 3)}h`,
|
|
332
|
+
schedule: 'friday-after-demo',
|
|
333
|
+
benefits: ['Mejor mantenibilidad', 'Menos bugs', 'Más testeable']
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Análisis de código duplicado
|
|
339
|
+
for (const dup of analysis.duplications) {
|
|
340
|
+
if (dup.similarity > 0.8 && dup.occurrences >= 3) {
|
|
341
|
+
tasks.push({
|
|
342
|
+
type: 'duplication',
|
|
343
|
+
title: `🔧 Extraer: ${dup.pattern}`,
|
|
344
|
+
priority: 'P2',
|
|
345
|
+
estimate: '2h',
|
|
346
|
+
schedule: 'friday-after-demo',
|
|
347
|
+
affectedFiles: dup.files
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return tasks;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Integra refactorización como PRs del stack
|
|
357
|
+
*/
|
|
358
|
+
createRefactoringStack(tasks: RefactoringTask[]): StackedPR[] {
|
|
359
|
+
// Agrupar refactorizaciones relacionadas
|
|
360
|
+
const groups = this.groupRelatedRefactorings(tasks);
|
|
361
|
+
|
|
362
|
+
return groups.map((group, index) => ({
|
|
363
|
+
stackOrder: index + 1,
|
|
364
|
+
branch: `refactor/${this.slugify(group.name)}`,
|
|
365
|
+
title: `🔧 Refactor: ${group.name}`,
|
|
366
|
+
type: 'technical',
|
|
367
|
+
learningObjectives: [
|
|
368
|
+
'Patrones de refactorización',
|
|
369
|
+
'Clean code principles',
|
|
370
|
+
'SOLID aplicado'
|
|
371
|
+
]
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Rutina Semanal del Freelancer
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
const WEEKLY_ROUTINE = {
|
|
381
|
+
monday: {
|
|
382
|
+
name: 'Planificación',
|
|
383
|
+
activities: [
|
|
384
|
+
'Review del backlog',
|
|
385
|
+
'Priorizar tareas de la semana',
|
|
386
|
+
'Actualizar tablero Kanban',
|
|
387
|
+
'Identificar stacks a trabajar',
|
|
388
|
+
'Comunicar plan al cliente (opcional)'
|
|
389
|
+
],
|
|
390
|
+
duration: '1-2h'
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
tuesdayToThursday: {
|
|
394
|
+
name: 'Desarrollo',
|
|
395
|
+
activities: [
|
|
396
|
+
'Tomar PR del stack (máximo 2 activos)',
|
|
397
|
+
'TDD en áreas críticas',
|
|
398
|
+
'Commits pequeños y frecuentes',
|
|
399
|
+
'Self-review antes de marcar ready',
|
|
400
|
+
'Mover siguiente PR del stack cuando se mergea'
|
|
401
|
+
],
|
|
402
|
+
wipLimit: 2,
|
|
403
|
+
focusTime: '4-6h/día'
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
friday: {
|
|
407
|
+
name: 'Review y Demo',
|
|
408
|
+
morning: [
|
|
409
|
+
'Code review personal (24h después de escribir)',
|
|
410
|
+
'Preparar demo',
|
|
411
|
+
'Actualizar documentación'
|
|
412
|
+
],
|
|
413
|
+
afternoon: [
|
|
414
|
+
'Demo al cliente (15-20 min)',
|
|
415
|
+
'Recoger feedback',
|
|
416
|
+
'Refactorización planificada (2h máx)',
|
|
417
|
+
'Retrospectiva personal'
|
|
418
|
+
],
|
|
419
|
+
demoFormat: {
|
|
420
|
+
duration: '15-20 min',
|
|
421
|
+
structure: [
|
|
422
|
+
'5 min: Qué se completó (PRs mergeados)',
|
|
423
|
+
'10 min: Demo de funcionalidad',
|
|
424
|
+
'5 min: Próximos pasos y feedback'
|
|
425
|
+
]
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 🌳 GitFlow: Estrategia de Branching
|
|
434
|
+
|
|
435
|
+
### Estructura de Ramas
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
main (producción)
|
|
439
|
+
│
|
|
440
|
+
├── hotfix/urgent-fix ──────────────────────────┐
|
|
441
|
+
│ │
|
|
442
|
+
develop (integración) ◄─────────────────────────┤
|
|
443
|
+
│ │
|
|
444
|
+
├── release/v1.0.0 ─────────────────────────────┤
|
|
445
|
+
│ │
|
|
446
|
+
├── feature/user-auth ◄── Stack de PRs │
|
|
447
|
+
│ ├── feature/user-auth/01-db-schema │
|
|
448
|
+
│ ├── feature/user-auth/02-api-endpoints │
|
|
449
|
+
│ ├── feature/user-auth/03-jwt-middleware │
|
|
450
|
+
│ └── feature/user-auth/04-frontend-forms │
|
|
451
|
+
│ │
|
|
452
|
+
└── feature/payment-system ◄── Otro stack │
|
|
453
|
+
├── feature/payment-system/01-stripe-setup │
|
|
454
|
+
├── feature/payment-system/02-checkout-api │
|
|
455
|
+
└── feature/payment-system/03-webhooks │
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Reglas de GitFlow
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
const GITFLOW_RULES = {
|
|
462
|
+
branches: {
|
|
463
|
+
main: {
|
|
464
|
+
purpose: 'Código en producción',
|
|
465
|
+
protection: 'strict',
|
|
466
|
+
mergeFrom: ['release/*', 'hotfix/*'],
|
|
467
|
+
directCommits: false
|
|
468
|
+
},
|
|
469
|
+
develop: {
|
|
470
|
+
purpose: 'Integración de features completadas',
|
|
471
|
+
protection: 'standard',
|
|
472
|
+
mergeFrom: ['feature/*', 'release/*', 'hotfix/*'],
|
|
473
|
+
directCommits: false
|
|
474
|
+
},
|
|
475
|
+
'feature/*': {
|
|
476
|
+
purpose: 'Desarrollo de nuevas funcionalidades',
|
|
477
|
+
basedOn: 'develop',
|
|
478
|
+
mergeTo: 'develop',
|
|
479
|
+
naming: 'feature/{feature-name}/{stack-number}-{description}'
|
|
480
|
+
},
|
|
481
|
+
'release/*': {
|
|
482
|
+
purpose: 'Preparación de releases',
|
|
483
|
+
basedOn: 'develop',
|
|
484
|
+
mergeTo: ['main', 'develop'],
|
|
485
|
+
naming: 'release/v{major}.{minor}.{patch}'
|
|
486
|
+
},
|
|
487
|
+
'hotfix/*': {
|
|
488
|
+
purpose: 'Fixes urgentes en producción',
|
|
489
|
+
basedOn: 'main',
|
|
490
|
+
mergeTo: ['main', 'develop'],
|
|
491
|
+
naming: 'hotfix/{issue-id}-{description}'
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## 📚 Stacked PRs: La Estrategia
|
|
500
|
+
|
|
501
|
+
### ¿Qué son los Stacked PRs?
|
|
502
|
+
|
|
503
|
+
```
|
|
504
|
+
❌ ENFOQUE TRADICIONAL (PR Monolítico):
|
|
505
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
506
|
+
│ PR #45: "Implementar sistema de autenticación" │
|
|
507
|
+
│ ├── 2,500 líneas cambiadas │
|
|
508
|
+
│ ├── 45 archivos modificados │
|
|
509
|
+
│ ├── Review time: 3-5 días │
|
|
510
|
+
│ └── Conflictos frecuentes con develop │
|
|
511
|
+
│ │
|
|
512
|
+
│ Resultado: 😫 Reviews eternos, merge hell │
|
|
513
|
+
└─────────────────────────────────────────────────────────────┘
|
|
514
|
+
|
|
515
|
+
✅ ENFOQUE STACKED PRs (Este agente):
|
|
516
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
517
|
+
│ Stack: "Sistema de Autenticación" │
|
|
518
|
+
│ │
|
|
519
|
+
│ PR #45: "01-db-schema" (base) │
|
|
520
|
+
│ ├── 150 líneas, 3 archivos │
|
|
521
|
+
│ ├── Review: 30 min │
|
|
522
|
+
│ └── ✅ Merged │
|
|
523
|
+
│ │ │
|
|
524
|
+
│ ▼ │
|
|
525
|
+
│ PR #46: "02-api-endpoints" (depende de #45) │
|
|
526
|
+
│ ├── 200 líneas, 5 archivos │
|
|
527
|
+
│ ├── Review: 45 min │
|
|
528
|
+
│ └── ✅ Merged │
|
|
529
|
+
│ │ │
|
|
530
|
+
│ ▼ │
|
|
531
|
+
│ PR #47: "03-jwt-middleware" (depende de #46) │
|
|
532
|
+
│ ├── 180 líneas, 4 archivos │
|
|
533
|
+
│ └── 🔄 En review │
|
|
534
|
+
│ │ │
|
|
535
|
+
│ ▼ │
|
|
536
|
+
│ PR #48: "04-frontend-forms" (depende de #47) │
|
|
537
|
+
│ ├── 300 líneas, 8 archivos │
|
|
538
|
+
│ └── ⏳ Draft (esperando #47) │
|
|
539
|
+
│ │
|
|
540
|
+
│ Resultado: 🚀 Reviews rápidos, merge incremental │
|
|
541
|
+
└─────────────────────────────────────────────────────────────┘
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Beneficios de Stacked PRs
|
|
545
|
+
|
|
546
|
+
| Aspecto | PR Monolítico | Stacked PRs |
|
|
547
|
+
|---------|---------------|-------------|
|
|
548
|
+
| **Tamaño** | 1000+ líneas | 100-300 líneas |
|
|
549
|
+
| **Review time** | 3-5 días | 30-60 min cada |
|
|
550
|
+
| **Calidad review** | Superficial | Profundo |
|
|
551
|
+
| **Conflictos** | Frecuentes | Raros |
|
|
552
|
+
| **Rollback** | Todo o nada | Granular |
|
|
553
|
+
| **Feedback** | Al final | Continuo |
|
|
554
|
+
| **Aprendizaje** | Tardío | Inmediato |
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## 🔧 Implementación del Sistema
|
|
559
|
+
|
|
560
|
+
### 1. Estructura de una Iteración con Stacked PRs
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
interface StackedIteration {
|
|
564
|
+
iterationNumber: number;
|
|
565
|
+
feature: string;
|
|
566
|
+
|
|
567
|
+
// El stack de PRs para esta iteración
|
|
568
|
+
prStack: StackedPR[];
|
|
569
|
+
|
|
570
|
+
// Configuración del stack
|
|
571
|
+
stackConfig: {
|
|
572
|
+
baseBranch: 'develop';
|
|
573
|
+
featureBranch: string; // feature/{feature-name}
|
|
574
|
+
namingPattern: '{feature}/{number}-{slug}';
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
interface StackedPR {
|
|
579
|
+
stackOrder: number; // Posición en el stack (01, 02, 03...)
|
|
580
|
+
branch: string; // feature/auth/01-db-schema
|
|
581
|
+
dependsOn: string | null; // Branch del que depende
|
|
582
|
+
|
|
583
|
+
// Contenido del PR
|
|
584
|
+
title: string;
|
|
585
|
+
description: string;
|
|
586
|
+
changes: FileChange[];
|
|
587
|
+
|
|
588
|
+
// Metadatos
|
|
589
|
+
estimatedReviewTime: string;
|
|
590
|
+
linesChanged: number;
|
|
591
|
+
filesAffected: number;
|
|
592
|
+
|
|
593
|
+
// Aprendizaje (de v3)
|
|
594
|
+
learningObjectives: string[];
|
|
595
|
+
conceptsIntroduced: string[];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Ejemplo de un stack completo
|
|
599
|
+
const authenticationStack: StackedIteration = {
|
|
600
|
+
iterationNumber: 1,
|
|
601
|
+
feature: 'user-authentication',
|
|
602
|
+
stackConfig: {
|
|
603
|
+
baseBranch: 'develop',
|
|
604
|
+
featureBranch: 'feature/user-auth',
|
|
605
|
+
namingPattern: 'feature/user-auth/{number}-{slug}'
|
|
606
|
+
},
|
|
607
|
+
prStack: [
|
|
608
|
+
{
|
|
609
|
+
stackOrder: 1,
|
|
610
|
+
branch: 'feature/user-auth/01-db-schema',
|
|
611
|
+
dependsOn: null, // Base del stack
|
|
612
|
+
title: '01: Database schema for users',
|
|
613
|
+
description: 'Add User model and migrations',
|
|
614
|
+
estimatedReviewTime: '30 min',
|
|
615
|
+
linesChanged: 150,
|
|
616
|
+
filesAffected: 4,
|
|
617
|
+
learningObjectives: ['Database migrations', 'User model design'],
|
|
618
|
+
conceptsIntroduced: ['Prisma schema', 'Database relations']
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
stackOrder: 2,
|
|
622
|
+
branch: 'feature/user-auth/02-api-endpoints',
|
|
623
|
+
dependsOn: 'feature/user-auth/01-db-schema',
|
|
624
|
+
title: '02: Auth API endpoints',
|
|
625
|
+
description: 'POST /auth/register, POST /auth/login',
|
|
626
|
+
estimatedReviewTime: '45 min',
|
|
627
|
+
linesChanged: 200,
|
|
628
|
+
filesAffected: 6,
|
|
629
|
+
learningObjectives: ['REST API design', 'Input validation'],
|
|
630
|
+
conceptsIntroduced: ['Zod validation', 'API routes']
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
stackOrder: 3,
|
|
634
|
+
branch: 'feature/user-auth/03-jwt-middleware',
|
|
635
|
+
dependsOn: 'feature/user-auth/02-api-endpoints',
|
|
636
|
+
title: '03: JWT authentication middleware',
|
|
637
|
+
description: 'Token generation and validation',
|
|
638
|
+
estimatedReviewTime: '45 min',
|
|
639
|
+
linesChanged: 180,
|
|
640
|
+
filesAffected: 5,
|
|
641
|
+
learningObjectives: ['JWT tokens', 'Middleware pattern'],
|
|
642
|
+
conceptsIntroduced: ['jose library', 'Auth middleware']
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
stackOrder: 4,
|
|
646
|
+
branch: 'feature/user-auth/04-frontend-forms',
|
|
647
|
+
dependsOn: 'feature/user-auth/03-jwt-middleware',
|
|
648
|
+
title: '04: Login and register forms',
|
|
649
|
+
description: 'React components with form validation',
|
|
650
|
+
estimatedReviewTime: '60 min',
|
|
651
|
+
linesChanged: 350,
|
|
652
|
+
filesAffected: 10,
|
|
653
|
+
learningObjectives: ['React forms', 'Client-side validation'],
|
|
654
|
+
conceptsIntroduced: ['React Hook Form', 'Toast notifications']
|
|
655
|
+
}
|
|
656
|
+
]
|
|
657
|
+
};
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### 2. Generador de Stacked PRs
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
class StackedPRGenerator {
|
|
664
|
+
private githubMCP: GitHubMCPClient;
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Genera un stack completo de PRs para una feature
|
|
668
|
+
*/
|
|
669
|
+
async generateFeatureStack(
|
|
670
|
+
repo: Repository,
|
|
671
|
+
feature: FeatureAnalysis,
|
|
672
|
+
iteration: Iteration
|
|
673
|
+
): Promise<PRStack> {
|
|
674
|
+
|
|
675
|
+
// 1. Dividir la feature en chunks lógicos
|
|
676
|
+
const chunks = await this.divideFeatureIntoChunks(feature);
|
|
677
|
+
|
|
678
|
+
// 2. Crear la rama base de la feature
|
|
679
|
+
const featureBranch = `feature/${this.slugify(feature.name)}`;
|
|
680
|
+
await this.createBranch(repo, featureBranch, 'develop');
|
|
681
|
+
|
|
682
|
+
// 3. Generar cada PR del stack
|
|
683
|
+
const stack: StackedPR[] = [];
|
|
684
|
+
let previousBranch = 'develop';
|
|
685
|
+
|
|
686
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
687
|
+
const chunk = chunks[i];
|
|
688
|
+
const stackNumber = String(i + 1).padStart(2, '0');
|
|
689
|
+
const branchName = `${featureBranch}/${stackNumber}-${chunk.slug}`;
|
|
690
|
+
|
|
691
|
+
// Crear branch basada en la anterior
|
|
692
|
+
await this.createBranch(repo, branchName, previousBranch);
|
|
693
|
+
|
|
694
|
+
// Generar el PR (como draft si no es el primero)
|
|
695
|
+
const pr = await this.createStackedPR(repo, {
|
|
696
|
+
branch: branchName,
|
|
697
|
+
base: previousBranch,
|
|
698
|
+
title: `[${iteration.number}/${stackNumber}] ${chunk.title}`,
|
|
699
|
+
body: this.generatePRBody(chunk, i, chunks.length, feature),
|
|
700
|
+
draft: i > 0, // Solo el primero no es draft
|
|
701
|
+
labels: this.generateLabels(chunk, stackNumber),
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
stack.push({
|
|
705
|
+
...pr,
|
|
706
|
+
stackOrder: i + 1,
|
|
707
|
+
dependsOn: i === 0 ? null : stack[i - 1].branch,
|
|
708
|
+
chunk: chunk
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
previousBranch = branchName;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// 4. Crear issue de tracking del stack
|
|
715
|
+
await this.createStackTrackingIssue(repo, feature, stack);
|
|
716
|
+
|
|
717
|
+
return { feature, stack, featureBranch };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Divide una feature en chunks reviewables
|
|
722
|
+
* Regla: Máximo 300 líneas por PR, máximo 10 archivos
|
|
723
|
+
*/
|
|
724
|
+
private async divideFeatureIntoChunks(feature: FeatureAnalysis): Promise<Chunk[]> {
|
|
725
|
+
const chunks: Chunk[] = [];
|
|
726
|
+
|
|
727
|
+
// Estrategia de división por capas
|
|
728
|
+
const layers = [
|
|
729
|
+
{ name: 'database', pattern: ['schema', 'migrations', 'models'] },
|
|
730
|
+
{ name: 'backend', pattern: ['api', 'services', 'middleware'] },
|
|
731
|
+
{ name: 'frontend', pattern: ['components', 'hooks', 'pages'] },
|
|
732
|
+
{ name: 'tests', pattern: ['test', 'spec', '__tests__'] },
|
|
733
|
+
];
|
|
734
|
+
|
|
735
|
+
for (const layer of layers) {
|
|
736
|
+
const layerChanges = feature.changes.filter(c =>
|
|
737
|
+
layer.pattern.some(p => c.path.includes(p))
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
if (layerChanges.length > 0) {
|
|
741
|
+
// Si el layer es muy grande, subdividir
|
|
742
|
+
if (this.calculateLines(layerChanges) > 300) {
|
|
743
|
+
chunks.push(...this.subdivideLayer(layer.name, layerChanges));
|
|
744
|
+
} else {
|
|
745
|
+
chunks.push({
|
|
746
|
+
slug: layer.name,
|
|
747
|
+
title: this.generateChunkTitle(layer.name, feature),
|
|
748
|
+
changes: layerChanges,
|
|
749
|
+
layer: layer.name
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return this.orderChunksByDependency(chunks);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Genera el body del PR con formato de stack
|
|
760
|
+
*/
|
|
761
|
+
private generatePRBody(
|
|
762
|
+
chunk: Chunk,
|
|
763
|
+
index: number,
|
|
764
|
+
total: number,
|
|
765
|
+
feature: FeatureAnalysis
|
|
766
|
+
): string {
|
|
767
|
+
return `
|
|
768
|
+
## 📚 Stack Progress: ${index + 1}/${total}
|
|
769
|
+
|
|
770
|
+
\`\`\`
|
|
771
|
+
${this.generateStackVisualization(index, total)}
|
|
772
|
+
\`\`\`
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
## 🎯 Objetivo de este PR
|
|
777
|
+
|
|
778
|
+
${chunk.description}
|
|
779
|
+
|
|
780
|
+
## 📦 Parte del Stack: "${feature.name}"
|
|
781
|
+
|
|
782
|
+
| # | PR | Estado |
|
|
783
|
+
|---|-----|--------|
|
|
784
|
+
${this.generateStackTable(index, total)}
|
|
785
|
+
|
|
786
|
+
## 🔗 Dependencias
|
|
787
|
+
|
|
788
|
+
${index === 0
|
|
789
|
+
? '✅ Este es el **base** del stack - no tiene dependencias'
|
|
790
|
+
: `⬆️ Depende de: PR anterior en el stack`
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
${index < total - 1
|
|
794
|
+
? `⬇️ Siguiente PR depende de este`
|
|
795
|
+
: `🏁 Este es el **último** PR del stack`
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
## 📚 Lo que Aprenderás (Aprendizaje Progresivo)
|
|
801
|
+
|
|
802
|
+
${chunk.learningObjectives?.map(obj => `- ${obj}`).join('\n') || 'N/A'}
|
|
803
|
+
|
|
804
|
+
## 📖 Conceptos Introducidos
|
|
805
|
+
|
|
806
|
+
${chunk.conceptsIntroduced?.map(c => `\`${c}\``).join(' • ') || 'N/A'}
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
## ✅ Checklist
|
|
811
|
+
|
|
812
|
+
- [ ] Código sigue las convenciones del proyecto
|
|
813
|
+
- [ ] Tests agregados/actualizados
|
|
814
|
+
- [ ] Sin console.logs o código de debug
|
|
815
|
+
- [ ] PR es pequeño y enfocado (~${chunk.estimatedLines} líneas)
|
|
816
|
+
|
|
817
|
+
## 🧪 Cómo Probar
|
|
818
|
+
|
|
819
|
+
\`\`\`bash
|
|
820
|
+
# Checkout este stack
|
|
821
|
+
git fetch origin ${chunk.branch}
|
|
822
|
+
git checkout ${chunk.branch}
|
|
823
|
+
|
|
824
|
+
# Ejecutar tests
|
|
825
|
+
npm test
|
|
826
|
+
|
|
827
|
+
# Verificar localmente
|
|
828
|
+
npm run dev
|
|
829
|
+
\`\`\`
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
## 📝 Notas para el Reviewer
|
|
834
|
+
|
|
835
|
+
${chunk.reviewNotes || 'Revisar los cambios y aprobar si todo está correcto.'}
|
|
836
|
+
|
|
837
|
+
---
|
|
838
|
+
|
|
839
|
+
_🔗 Este PR es parte de un **Stacked PR**. Por favor, revisar en orden._
|
|
840
|
+
_📚 Generado con freelance-project-planner-v4_
|
|
841
|
+
`;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Visualización ASCII del progreso del stack
|
|
846
|
+
*/
|
|
847
|
+
private generateStackVisualization(current: number, total: number): string {
|
|
848
|
+
let viz = '';
|
|
849
|
+
for (let i = 0; i < total; i++) {
|
|
850
|
+
const status = i < current ? '✅' : i === current ? '👉' : '⏳';
|
|
851
|
+
const connector = i < total - 1 ? '\n │\n ▼\n' : '';
|
|
852
|
+
viz += `${status} PR ${String(i + 1).padStart(2, '0')}${connector}`;
|
|
853
|
+
}
|
|
854
|
+
return viz;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Crea issue de tracking para el stack completo
|
|
859
|
+
*/
|
|
860
|
+
private async createStackTrackingIssue(
|
|
861
|
+
repo: Repository,
|
|
862
|
+
feature: FeatureAnalysis,
|
|
863
|
+
stack: StackedPR[]
|
|
864
|
+
): Promise<Issue> {
|
|
865
|
+
const body = `
|
|
866
|
+
# 📚 Stack Tracking: ${feature.name}
|
|
867
|
+
|
|
868
|
+
## 📊 Estado del Stack
|
|
869
|
+
|
|
870
|
+
| # | PR | Branch | Estado | Lines |
|
|
871
|
+
|---|-----|--------|--------|-------|
|
|
872
|
+
${stack.map((pr, i) =>
|
|
873
|
+
`| ${i + 1} | #${pr.number} | \`${pr.branch}\` | ${i === 0 ? '🔄 Review' : '📝 Draft'} | ~${pr.linesChanged} |`
|
|
874
|
+
).join('\n')}
|
|
875
|
+
|
|
876
|
+
## 📈 Progreso
|
|
877
|
+
|
|
878
|
+
\`\`\`
|
|
879
|
+
[${stack.map((_, i) => i === 0 ? '🔄' : '⬜').join('')}] 0/${stack.length} merged
|
|
880
|
+
\`\`\`
|
|
881
|
+
|
|
882
|
+
## 🔗 Orden de Merge
|
|
883
|
+
|
|
884
|
+
1. Merge PR #${stack[0].number} a \`develop\`
|
|
885
|
+
2. Actualizar base de PR #${stack[1]?.number || 'N/A'} a \`develop\`
|
|
886
|
+
3. Repetir hasta completar el stack
|
|
887
|
+
|
|
888
|
+
## 📚 Aprendizaje Acumulado
|
|
889
|
+
|
|
890
|
+
Al completar este stack, habrás aprendido:
|
|
891
|
+
|
|
892
|
+
${stack.flatMap(pr => pr.learningObjectives || []).map(obj => `- ${obj}`).join('\n')}
|
|
893
|
+
|
|
894
|
+
## ⚠️ Notas
|
|
895
|
+
|
|
896
|
+
- Los PRs deben mergearse **en orden**
|
|
897
|
+
- Después de mergear uno, actualizar el base del siguiente
|
|
898
|
+
- Si hay conflictos, resolverlos antes de continuar
|
|
899
|
+
|
|
900
|
+
---
|
|
901
|
+
|
|
902
|
+
_Este issue se actualiza automáticamente al mergear PRs del stack_
|
|
903
|
+
`;
|
|
904
|
+
|
|
905
|
+
return await this.githubMCP.createIssue(repo, {
|
|
906
|
+
title: `📚 Stack: ${feature.name}`,
|
|
907
|
+
body: body,
|
|
908
|
+
labels: ['stack-tracking', 'feature'],
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
### 3. GitHub Actions para Stacked PRs
|
|
915
|
+
|
|
916
|
+
```yaml
|
|
917
|
+
# .github/workflows/stacked-pr-ci.yml
|
|
918
|
+
name: Stacked PR CI
|
|
919
|
+
|
|
920
|
+
on:
|
|
921
|
+
pull_request:
|
|
922
|
+
types: [opened, synchronize, reopened]
|
|
923
|
+
|
|
924
|
+
jobs:
|
|
925
|
+
# Detectar si es parte de un stack
|
|
926
|
+
detect-stack:
|
|
927
|
+
runs-on: ubuntu-latest
|
|
928
|
+
outputs:
|
|
929
|
+
is_stacked: ${{ steps.detect.outputs.is_stacked }}
|
|
930
|
+
stack_position: ${{ steps.detect.outputs.stack_position }}
|
|
931
|
+
stack_base: ${{ steps.detect.outputs.stack_base }}
|
|
932
|
+
steps:
|
|
933
|
+
- name: Detect Stacked PR
|
|
934
|
+
id: detect
|
|
935
|
+
run: |
|
|
936
|
+
BRANCH="${{ github.head_ref }}"
|
|
937
|
+
|
|
938
|
+
# Pattern: feature/{name}/{number}-{slug}
|
|
939
|
+
if [[ $BRANCH =~ ^feature/([^/]+)/([0-9]+)-(.+)$ ]]; then
|
|
940
|
+
echo "is_stacked=true" >> $GITHUB_OUTPUT
|
|
941
|
+
echo "stack_position=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
|
|
942
|
+
echo "stack_base=feature/${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
|
|
943
|
+
else
|
|
944
|
+
echo "is_stacked=false" >> $GITHUB_OUTPUT
|
|
945
|
+
fi
|
|
946
|
+
|
|
947
|
+
# Tests estándar
|
|
948
|
+
test:
|
|
949
|
+
runs-on: ubuntu-latest
|
|
950
|
+
steps:
|
|
951
|
+
- uses: actions/checkout@v4
|
|
952
|
+
|
|
953
|
+
- name: Setup Node.js
|
|
954
|
+
uses: actions/setup-node@v4
|
|
955
|
+
with:
|
|
956
|
+
node-version: '20'
|
|
957
|
+
cache: 'npm'
|
|
958
|
+
|
|
959
|
+
- run: npm ci
|
|
960
|
+
- run: npm run lint
|
|
961
|
+
- run: npm run type-check
|
|
962
|
+
- run: npm run test
|
|
963
|
+
|
|
964
|
+
# Validación específica para stacked PRs
|
|
965
|
+
validate-stack:
|
|
966
|
+
runs-on: ubuntu-latest
|
|
967
|
+
needs: detect-stack
|
|
968
|
+
if: needs.detect-stack.outputs.is_stacked == 'true'
|
|
969
|
+
steps:
|
|
970
|
+
- uses: actions/checkout@v4
|
|
971
|
+
with:
|
|
972
|
+
fetch-depth: 0
|
|
973
|
+
|
|
974
|
+
- name: Validate Stack Order
|
|
975
|
+
run: |
|
|
976
|
+
POSITION=${{ needs.detect-stack.outputs.stack_position }}
|
|
977
|
+
BASE=${{ needs.detect-stack.outputs.stack_base }}
|
|
978
|
+
|
|
979
|
+
echo "📚 Stacked PR detectado"
|
|
980
|
+
echo " Posición: $POSITION"
|
|
981
|
+
echo " Stack base: $BASE"
|
|
982
|
+
|
|
983
|
+
# Verificar que los PRs anteriores existen
|
|
984
|
+
if [ "$POSITION" -gt "01" ]; then
|
|
985
|
+
PREV_POSITION=$(printf "%02d" $((10#$POSITION - 1)))
|
|
986
|
+
echo " Verificando PR anterior: ${PREV_POSITION}"
|
|
987
|
+
|
|
988
|
+
# Verificar que la rama anterior existe
|
|
989
|
+
if ! git ls-remote --heads origin | grep -q "${BASE}/${PREV_POSITION}"; then
|
|
990
|
+
echo "⚠️ Warning: PR anterior no encontrado"
|
|
991
|
+
fi
|
|
992
|
+
fi
|
|
993
|
+
|
|
994
|
+
- name: Check PR Size
|
|
995
|
+
run: |
|
|
996
|
+
LINES_CHANGED=$(git diff --shortstat origin/${{ github.base_ref }}...HEAD | grep -oP '\d+(?= insertion)' || echo "0")
|
|
997
|
+
|
|
998
|
+
echo "📊 Líneas cambiadas: $LINES_CHANGED"
|
|
999
|
+
|
|
1000
|
+
if [ "$LINES_CHANGED" -gt 400 ]; then
|
|
1001
|
+
echo "⚠️ Warning: PR muy grande para un stack ($LINES_CHANGED líneas)"
|
|
1002
|
+
echo " Considera dividirlo más"
|
|
1003
|
+
else
|
|
1004
|
+
echo "✅ Tamaño del PR apropiado para review"
|
|
1005
|
+
fi
|
|
1006
|
+
|
|
1007
|
+
# Comentario automático con info del stack
|
|
1008
|
+
stack-comment:
|
|
1009
|
+
runs-on: ubuntu-latest
|
|
1010
|
+
needs: [detect-stack, test]
|
|
1011
|
+
if: needs.detect-stack.outputs.is_stacked == 'true'
|
|
1012
|
+
permissions:
|
|
1013
|
+
pull-requests: write
|
|
1014
|
+
steps:
|
|
1015
|
+
- uses: actions/github-script@v7
|
|
1016
|
+
with:
|
|
1017
|
+
script: |
|
|
1018
|
+
const position = '${{ needs.detect-stack.outputs.stack_position }}';
|
|
1019
|
+
const stackBase = '${{ needs.detect-stack.outputs.stack_base }}';
|
|
1020
|
+
|
|
1021
|
+
const comment = `
|
|
1022
|
+
## 📚 Stacked PR Info
|
|
1023
|
+
|
|
1024
|
+
| Propiedad | Valor |
|
|
1025
|
+
|-----------|-------|
|
|
1026
|
+
| **Stack** | \`${stackBase}\` |
|
|
1027
|
+
| **Posición** | #${position} |
|
|
1028
|
+
| **CI Status** | ✅ Passed |
|
|
1029
|
+
|
|
1030
|
+
### 📋 Checklist de Merge
|
|
1031
|
+
|
|
1032
|
+
${position === '01' ?
|
|
1033
|
+
'- [ ] Este es el **primer PR** del stack - puede mergearse directamente' :
|
|
1034
|
+
'- [ ] Verificar que el PR anterior está mergeado\n- [ ] Actualizar base branch si es necesario'
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
---
|
|
1038
|
+
_🤖 Comentario automático de Stacked PR CI_
|
|
1039
|
+
`;
|
|
1040
|
+
|
|
1041
|
+
github.rest.issues.createComment({
|
|
1042
|
+
issue_number: context.issue.number,
|
|
1043
|
+
owner: context.repo.owner,
|
|
1044
|
+
repo: context.repo.repo,
|
|
1045
|
+
body: comment
|
|
1046
|
+
});
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
### 4. Workflow de Merge para Stacks
|
|
1050
|
+
|
|
1051
|
+
```typescript
|
|
1052
|
+
class StackMergeManager {
|
|
1053
|
+
/**
|
|
1054
|
+
* Maneja el merge de un PR dentro de un stack
|
|
1055
|
+
*/
|
|
1056
|
+
async handleStackMerge(
|
|
1057
|
+
repo: Repository,
|
|
1058
|
+
mergedPR: PullRequest
|
|
1059
|
+
): Promise<void> {
|
|
1060
|
+
const stackInfo = this.parseStackBranch(mergedPR.head.ref);
|
|
1061
|
+
|
|
1062
|
+
if (!stackInfo) return; // No es un stacked PR
|
|
1063
|
+
|
|
1064
|
+
console.log(`📚 Stack PR merged: ${mergedPR.title}`);
|
|
1065
|
+
console.log(` Stack: ${stackInfo.feature}`);
|
|
1066
|
+
console.log(` Position: ${stackInfo.position}`);
|
|
1067
|
+
|
|
1068
|
+
// 1. Encontrar el siguiente PR en el stack
|
|
1069
|
+
const nextBranch = this.getNextStackBranch(stackInfo);
|
|
1070
|
+
const nextPR = await this.findPRByBranch(repo, nextBranch);
|
|
1071
|
+
|
|
1072
|
+
if (nextPR) {
|
|
1073
|
+
// 2. Actualizar el base del siguiente PR
|
|
1074
|
+
await this.githubMCP.updatePullRequest(repo, nextPR.number, {
|
|
1075
|
+
base: 'develop' // Ahora apunta a develop en lugar del PR mergeado
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
// 3. Quitar el draft status
|
|
1079
|
+
await this.githubMCP.updatePullRequest(repo, nextPR.number, {
|
|
1080
|
+
draft: false
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// 4. Agregar comentario
|
|
1084
|
+
await this.githubMCP.addComment(repo, nextPR.number, `
|
|
1085
|
+
🎉 **PR anterior mergeado!**
|
|
1086
|
+
|
|
1087
|
+
Este PR ahora está listo para review. El base ha sido actualizado a \`develop\`.
|
|
1088
|
+
|
|
1089
|
+
📊 Stack Progress: ${stackInfo.position}/${stackInfo.total} completado
|
|
1090
|
+
`);
|
|
1091
|
+
|
|
1092
|
+
console.log(` ✅ Next PR #${nextPR.number} actualizado y listo para review`);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// 5. Actualizar issue de tracking
|
|
1096
|
+
await this.updateStackTrackingIssue(repo, stackInfo, mergedPR);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Actualiza el issue de tracking del stack
|
|
1101
|
+
*/
|
|
1102
|
+
private async updateStackTrackingIssue(
|
|
1103
|
+
repo: Repository,
|
|
1104
|
+
stackInfo: StackInfo,
|
|
1105
|
+
mergedPR: PullRequest
|
|
1106
|
+
): Promise<void> {
|
|
1107
|
+
const trackingIssue = await this.findStackTrackingIssue(repo, stackInfo.feature);
|
|
1108
|
+
|
|
1109
|
+
if (!trackingIssue) return;
|
|
1110
|
+
|
|
1111
|
+
// Actualizar el body con el nuevo estado
|
|
1112
|
+
const newBody = this.updateStackProgressInBody(
|
|
1113
|
+
trackingIssue.body,
|
|
1114
|
+
stackInfo.position,
|
|
1115
|
+
'merged'
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
await this.githubMCP.updateIssue(repo, trackingIssue.number, {
|
|
1119
|
+
body: newBody
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
// Si es el último PR, cerrar el tracking issue
|
|
1123
|
+
if (stackInfo.position === stackInfo.total) {
|
|
1124
|
+
await this.githubMCP.updateIssue(repo, trackingIssue.number, {
|
|
1125
|
+
state: 'closed'
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
await this.githubMCP.addComment(repo, trackingIssue.number, `
|
|
1129
|
+
🎉 **Stack Completado!**
|
|
1130
|
+
|
|
1131
|
+
Todos los PRs del stack han sido mergeados exitosamente.
|
|
1132
|
+
|
|
1133
|
+
## 📚 Resumen de Aprendizaje
|
|
1134
|
+
|
|
1135
|
+
Durante este stack, aprendiste:
|
|
1136
|
+
${stackInfo.allLearningObjectives.map(obj => `- ✅ ${obj}`).join('\n')}
|
|
1137
|
+
|
|
1138
|
+
---
|
|
1139
|
+
|
|
1140
|
+
_Stack completado en ${this.calculateStackDuration(stackInfo)}_
|
|
1141
|
+
`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
---
|
|
1148
|
+
|
|
1149
|
+
## 🐳 Docker First (Heredado de v3)
|
|
1150
|
+
|
|
1151
|
+
### Generación Automática de Docker
|
|
1152
|
+
|
|
1153
|
+
Se mantiene toda la funcionalidad de v3 para Docker:
|
|
1154
|
+
- Dockerfile multi-stage
|
|
1155
|
+
- docker-compose para dev y prod
|
|
1156
|
+
- .dockerignore optimizado
|
|
1157
|
+
- Scripts de conveniencia
|
|
1158
|
+
|
|
1159
|
+
```typescript
|
|
1160
|
+
// Ver implementación completa en v3
|
|
1161
|
+
// Esta versión hereda todo el sistema Docker de v3
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
---
|
|
1165
|
+
|
|
1166
|
+
## 📚 Aprendizaje Progresivo en Stacked PRs
|
|
1167
|
+
|
|
1168
|
+
### Integración de Aprendizaje por PR
|
|
1169
|
+
|
|
1170
|
+
Cada PR del stack incluye su propio contexto de aprendizaje:
|
|
1171
|
+
|
|
1172
|
+
```markdown
|
|
1173
|
+
## 📚 Stack: Sistema de Autenticación
|
|
1174
|
+
|
|
1175
|
+
### PR 01/04: Database Schema
|
|
1176
|
+
|
|
1177
|
+
#### 🎯 Objetivo
|
|
1178
|
+
Crear el esquema de base de datos para usuarios.
|
|
1179
|
+
|
|
1180
|
+
#### 📚 Lo que Aprenderás
|
|
1181
|
+
- Diseño de modelos de datos
|
|
1182
|
+
- Migrations en Prisma
|
|
1183
|
+
- Relaciones entre tablas
|
|
1184
|
+
|
|
1185
|
+
#### 📖 Contexto Just-in-Time
|
|
1186
|
+
|
|
1187
|
+
<details>
|
|
1188
|
+
<summary>🗄️ ¿Qué son las migrations?</summary>
|
|
1189
|
+
|
|
1190
|
+
Las migrations son "versiones" de tu base de datos. Cada migration
|
|
1191
|
+
describe un cambio (crear tabla, agregar columna, etc.).
|
|
1192
|
+
|
|
1193
|
+
**¿Por qué importan?**
|
|
1194
|
+
- Trackean cambios en la BD como código
|
|
1195
|
+
- Permiten rollback si algo sale mal
|
|
1196
|
+
- Todos en el equipo tienen la misma estructura
|
|
1197
|
+
|
|
1198
|
+
**Comando clave:**
|
|
1199
|
+
```bash
|
|
1200
|
+
npx prisma migrate dev --name add_users_table
|
|
1201
|
+
```
|
|
1202
|
+
</details>
|
|
1203
|
+
|
|
1204
|
+
#### 🔗 Conexión con PRs Siguientes
|
|
1205
|
+
Este PR crea la base para:
|
|
1206
|
+
- PR 02: Usará el modelo User en los endpoints
|
|
1207
|
+
- PR 03: Agregará campos para tokens JWT
|
|
1208
|
+
- PR 04: Mostrará datos del usuario en el frontend
|
|
1209
|
+
|
|
1210
|
+
---
|
|
1211
|
+
|
|
1212
|
+
### PR 02/04: API Endpoints
|
|
1213
|
+
|
|
1214
|
+
#### 🎯 Objetivo
|
|
1215
|
+
Crear endpoints de registro y login.
|
|
1216
|
+
|
|
1217
|
+
#### 📚 Lo que Aprenderás
|
|
1218
|
+
- Diseño de APIs REST
|
|
1219
|
+
- Validación de inputs
|
|
1220
|
+
- Manejo de errores
|
|
1221
|
+
|
|
1222
|
+
#### 🔗 Construyendo sobre PR 01
|
|
1223
|
+
Ahora que tienes el modelo User, puedes:
|
|
1224
|
+
- Crear usuarios en la base de datos
|
|
1225
|
+
- Validar credenciales
|
|
1226
|
+
- Retornar datos del usuario
|
|
1227
|
+
|
|
1228
|
+
#### 📖 Contexto Just-in-Time
|
|
1229
|
+
|
|
1230
|
+
<details>
|
|
1231
|
+
<summary>🔒 ¿Por qué validar inputs?</summary>
|
|
1232
|
+
|
|
1233
|
+
**Nunca confíes en datos del cliente.**
|
|
1234
|
+
|
|
1235
|
+
```typescript
|
|
1236
|
+
// ❌ MAL: Sin validación
|
|
1237
|
+
app.post('/register', (req, res) => {
|
|
1238
|
+
const { email, password } = req.body;
|
|
1239
|
+
// ¿Y si email es un array? ¿Y si password es undefined?
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
// ✅ BIEN: Con Zod
|
|
1243
|
+
const registerSchema = z.object({
|
|
1244
|
+
email: z.string().email(),
|
|
1245
|
+
password: z.string().min(8)
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
app.post('/register', (req, res) => {
|
|
1249
|
+
const data = registerSchema.parse(req.body);
|
|
1250
|
+
// Ahora data.email y data.password son strings válidos
|
|
1251
|
+
});
|
|
1252
|
+
```
|
|
1253
|
+
</details>
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
### Mapa de Aprendizaje por Stack
|
|
1257
|
+
|
|
1258
|
+
```typescript
|
|
1259
|
+
class StackLearningTracker {
|
|
1260
|
+
generateLearningMap(stack: PRStack): string {
|
|
1261
|
+
return `
|
|
1262
|
+
# 🗺️ Mapa de Aprendizaje - Stack: ${stack.feature}
|
|
1263
|
+
|
|
1264
|
+
\`\`\`
|
|
1265
|
+
PR 01: Database Schema
|
|
1266
|
+
├── 📚 Prisma migrations
|
|
1267
|
+
├── 📚 Diseño de modelos
|
|
1268
|
+
└── ✅ Completado
|
|
1269
|
+
│
|
|
1270
|
+
▼
|
|
1271
|
+
PR 02: API Endpoints
|
|
1272
|
+
├── 📚 REST API design
|
|
1273
|
+
├── 📚 Zod validation
|
|
1274
|
+
├── 📚 Error handling
|
|
1275
|
+
└── 🔄 En review
|
|
1276
|
+
│
|
|
1277
|
+
▼
|
|
1278
|
+
PR 03: JWT Middleware
|
|
1279
|
+
├── 📚 JSON Web Tokens
|
|
1280
|
+
├── 📚 Middleware pattern
|
|
1281
|
+
├── 📚 Auth flow
|
|
1282
|
+
└── ⏳ Esperando
|
|
1283
|
+
│
|
|
1284
|
+
▼
|
|
1285
|
+
PR 04: Frontend Forms
|
|
1286
|
+
├── 📚 React Hook Form
|
|
1287
|
+
├── 📚 Client validation
|
|
1288
|
+
├── 📚 Toast notifications
|
|
1289
|
+
└── ⏳ Draft
|
|
1290
|
+
|
|
1291
|
+
\`\`\`
|
|
1292
|
+
|
|
1293
|
+
## 📊 Progreso de Aprendizaje
|
|
1294
|
+
|
|
1295
|
+
| Concepto | PR | Estado |
|
|
1296
|
+
|----------|-----|--------|
|
|
1297
|
+
| Prisma migrations | 01 | ✅ |
|
|
1298
|
+
| Diseño de modelos | 01 | ✅ |
|
|
1299
|
+
| REST API design | 02 | 🔄 |
|
|
1300
|
+
| Zod validation | 02 | 🔄 |
|
|
1301
|
+
| JWT tokens | 03 | ⏳ |
|
|
1302
|
+
| Middleware | 03 | ⏳ |
|
|
1303
|
+
| React forms | 04 | ⏳ |
|
|
1304
|
+
|
|
1305
|
+
**Conceptos dominados: 2/7 (29%)**
|
|
1306
|
+
`;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
---
|
|
1312
|
+
|
|
1313
|
+
## 🚀 Workflow Completo del Agente
|
|
1314
|
+
|
|
1315
|
+
```typescript
|
|
1316
|
+
class FreelancePlannerV4 {
|
|
1317
|
+
async executeFull(
|
|
1318
|
+
projectPath: string,
|
|
1319
|
+
options: PlannerOptions
|
|
1320
|
+
): Promise<ExecutionResult> {
|
|
1321
|
+
console.log('🚀 Freelance Project Planner v4.0');
|
|
1322
|
+
console.log('📚 GitFlow + Stacked PRs + Infrastructure First\n');
|
|
1323
|
+
|
|
1324
|
+
// FASE 0: Análisis
|
|
1325
|
+
console.log('📊 FASE 0: Análisis del Proyecto');
|
|
1326
|
+
const analysis = await this.analyzer.analyzeProject(projectPath);
|
|
1327
|
+
|
|
1328
|
+
// FASE 1: Docker (prioridad máxima - heredado de v3)
|
|
1329
|
+
console.log('\n🐳 FASE 1: Dockerización');
|
|
1330
|
+
const dockerSetup = await this.dockerGenerator.generateDockerSetup(analysis);
|
|
1331
|
+
|
|
1332
|
+
// FASE 2: GitHub Actions con soporte para stacks
|
|
1333
|
+
console.log('\n⚙️ FASE 2: GitHub Actions (con Stacked PR support)');
|
|
1334
|
+
const workflows = await this.generateStackAwareWorkflows(analysis);
|
|
1335
|
+
|
|
1336
|
+
// FASE 3: GitFlow Setup
|
|
1337
|
+
console.log('\n🌳 FASE 3: GitFlow Setup');
|
|
1338
|
+
await this.setupGitFlow(analysis.repo);
|
|
1339
|
+
|
|
1340
|
+
// FASE 4: Planificación en Stacked PRs
|
|
1341
|
+
console.log('\n📚 FASE 4: Generación de Iteraciones como Stacked PRs');
|
|
1342
|
+
const iterations = await this.planIterationsAsStacks(analysis);
|
|
1343
|
+
|
|
1344
|
+
// FASE 5: Crear PRs y tracking issues
|
|
1345
|
+
console.log('\n🔗 FASE 5: Creación de Stacked PRs en GitHub');
|
|
1346
|
+
const stacks = await this.createAllStacks(analysis.repo, iterations);
|
|
1347
|
+
|
|
1348
|
+
// Resumen final
|
|
1349
|
+
this.printExecutionSummary(analysis, dockerSetup, workflows, stacks);
|
|
1350
|
+
|
|
1351
|
+
return { analysis, dockerSetup, workflows, stacks };
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private async setupGitFlow(repo: Repository): Promise<void> {
|
|
1355
|
+
// Crear ramas principales
|
|
1356
|
+
const branches = ['develop', 'staging'];
|
|
1357
|
+
|
|
1358
|
+
for (const branch of branches) {
|
|
1359
|
+
try {
|
|
1360
|
+
await this.githubMCP.createBranch(repo, branch, 'main');
|
|
1361
|
+
console.log(` ✅ Rama creada: ${branch}`);
|
|
1362
|
+
} catch {
|
|
1363
|
+
console.log(` ℹ️ Rama ${branch} ya existe`);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// Configurar protecciones
|
|
1368
|
+
await this.setupBranchProtections(repo);
|
|
1369
|
+
|
|
1370
|
+
// Configurar reglas de merge
|
|
1371
|
+
await this.setupMergeRules(repo);
|
|
1372
|
+
|
|
1373
|
+
console.log(' ✅ GitFlow configurado');
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
private async planIterationsAsStacks(
|
|
1377
|
+
analysis: ProjectAnalysis
|
|
1378
|
+
): Promise<StackedIteration[]> {
|
|
1379
|
+
const features = await this.extractFeatures(analysis);
|
|
1380
|
+
const iterations: StackedIteration[] = [];
|
|
1381
|
+
|
|
1382
|
+
let iterationNumber = 1;
|
|
1383
|
+
for (const feature of features) {
|
|
1384
|
+
// Dividir cada feature en un stack de PRs
|
|
1385
|
+
const chunks = await this.divideFeatureIntoChunks(feature);
|
|
1386
|
+
|
|
1387
|
+
iterations.push({
|
|
1388
|
+
iterationNumber: iterationNumber++,
|
|
1389
|
+
feature: feature.name,
|
|
1390
|
+
description: feature.description,
|
|
1391
|
+
prStack: chunks.map((chunk, index) => ({
|
|
1392
|
+
stackOrder: index + 1,
|
|
1393
|
+
branch: `feature/${this.slugify(feature.name)}/${String(index + 1).padStart(2, '0')}-${chunk.slug}`,
|
|
1394
|
+
title: chunk.title,
|
|
1395
|
+
description: chunk.description,
|
|
1396
|
+
dependsOn: index === 0 ? null : chunks[index - 1].slug,
|
|
1397
|
+
estimatedLines: chunk.estimatedLines,
|
|
1398
|
+
learningObjectives: chunk.learningObjectives,
|
|
1399
|
+
conceptsIntroduced: chunk.conceptsIntroduced
|
|
1400
|
+
})),
|
|
1401
|
+
totalEstimatedReviewTime: this.calculateTotalReviewTime(chunks),
|
|
1402
|
+
learningPath: this.generateLearningPath(chunks)
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
console.log(` 📚 Iteración ${iterationNumber - 1}: ${feature.name}`);
|
|
1406
|
+
console.log(` └── ${chunks.length} PRs en el stack`);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
return iterations;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
private printExecutionSummary(
|
|
1413
|
+
analysis: ProjectAnalysis,
|
|
1414
|
+
dockerSetup: DockerSetup,
|
|
1415
|
+
workflows: Workflows,
|
|
1416
|
+
stacks: PRStack[]
|
|
1417
|
+
): void {
|
|
1418
|
+
const totalPRs = stacks.reduce((sum, s) => sum + s.prStack.length, 0);
|
|
1419
|
+
|
|
1420
|
+
console.log(`
|
|
1421
|
+
=====================================
|
|
1422
|
+
✅ Setup Completado - v4 GitFlow + Stacked PRs
|
|
1423
|
+
=====================================
|
|
1424
|
+
|
|
1425
|
+
🐳 Docker:
|
|
1426
|
+
- Dockerfile (multi-stage)
|
|
1427
|
+
- docker-compose.dev.yml
|
|
1428
|
+
- docker-compose.prod.yml
|
|
1429
|
+
|
|
1430
|
+
⚙️ GitHub Actions:
|
|
1431
|
+
- CI Pipeline
|
|
1432
|
+
- Stacked PR Validator
|
|
1433
|
+
- Auto-update bases on merge
|
|
1434
|
+
|
|
1435
|
+
🌳 GitFlow:
|
|
1436
|
+
- main (protected)
|
|
1437
|
+
- develop (integration)
|
|
1438
|
+
- feature/* (stacked PRs)
|
|
1439
|
+
|
|
1440
|
+
📚 Stacked PRs:
|
|
1441
|
+
- ${stacks.length} features planificadas
|
|
1442
|
+
- ${totalPRs} PRs totales
|
|
1443
|
+
- Review time estimado: ~${this.calculateTotalReviewTime(stacks)} horas
|
|
1444
|
+
|
|
1445
|
+
📋 Iteraciones:
|
|
1446
|
+
${stacks.map((s, i) => `
|
|
1447
|
+
Iteración ${i + 1}: ${s.feature}
|
|
1448
|
+
└── Stack de ${s.prStack.length} PRs:
|
|
1449
|
+
${s.prStack.map((pr, j) => ` ${j + 1}. ${pr.title} (~${pr.estimatedLines} líneas)`).join('\n')}
|
|
1450
|
+
`).join('')}
|
|
1451
|
+
|
|
1452
|
+
🎓 Aprendizaje Integrado:
|
|
1453
|
+
- Cada PR incluye contexto de aprendizaje
|
|
1454
|
+
- Conceptos introducidos progresivamente
|
|
1455
|
+
- Reflexión al completar cada stack
|
|
1456
|
+
|
|
1457
|
+
📚 Workflow Recomendado:
|
|
1458
|
+
1. Tomar el primer PR del stack
|
|
1459
|
+
2. Review y merge a develop
|
|
1460
|
+
3. CI actualiza automáticamente el siguiente PR
|
|
1461
|
+
4. Repetir hasta completar el stack
|
|
1462
|
+
5. Reflexionar sobre conceptos aprendidos
|
|
1463
|
+
|
|
1464
|
+
=====================================
|
|
1465
|
+
`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
---
|
|
1471
|
+
|
|
1472
|
+
## 📋 Comandos CLI
|
|
1473
|
+
|
|
1474
|
+
```bash
|
|
1475
|
+
# Setup completo con GitFlow y Stacked PRs
|
|
1476
|
+
freelance-planner-v4 setup ./mi-proyecto \
|
|
1477
|
+
--github-repo "usuario/proyecto" \
|
|
1478
|
+
--gitflow \
|
|
1479
|
+
--stacked-prs
|
|
1480
|
+
|
|
1481
|
+
# Crear stack para una feature específica
|
|
1482
|
+
freelance-planner-v4 create-stack ./mi-proyecto \
|
|
1483
|
+
--feature "user-authentication" \
|
|
1484
|
+
--github-repo "usuario/proyecto"
|
|
1485
|
+
|
|
1486
|
+
# Ver estado de todos los stacks
|
|
1487
|
+
freelance-planner-v4 stack-status ./mi-proyecto \
|
|
1488
|
+
--github-repo "usuario/proyecto"
|
|
1489
|
+
|
|
1490
|
+
# Merge el siguiente PR de un stack
|
|
1491
|
+
freelance-planner-v4 merge-next ./mi-proyecto \
|
|
1492
|
+
--stack "user-authentication"
|
|
1493
|
+
|
|
1494
|
+
# Generar reporte de aprendizaje
|
|
1495
|
+
freelance-planner-v4 learning-report ./mi-proyecto \
|
|
1496
|
+
--stack "user-authentication"
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
---
|
|
1500
|
+
|
|
1501
|
+
## 🎯 Resumen de Diferencias: v1 vs v2 vs v3 vs v4
|
|
1502
|
+
|
|
1503
|
+
| Aspecto | v1 | v2 | v3 | v4 |
|
|
1504
|
+
|---------|----|----|----|----|
|
|
1505
|
+
| **Modelo** | Sonnet | Sonnet | Opus 4.5 | Opus 4.5 |
|
|
1506
|
+
| **GitHub MCP** | ❌ | ✅ | ✅ | ✅ |
|
|
1507
|
+
| **Docker Priority** | ⚪ | ⚪ | 🟢 #1 | 🟢 #1 |
|
|
1508
|
+
| **Kanban + WIP** | ✅ | ✅ | ⚪ | ✅ |
|
|
1509
|
+
| **XP/TDD Selectivo** | ✅ | ✅ | ⚪ | ✅ |
|
|
1510
|
+
| **GitFlow** | ❌ | ❌ | ❌ | ✅ |
|
|
1511
|
+
| **Stacked PRs** | ❌ | ❌ | ❌ | ✅ |
|
|
1512
|
+
| **Aprendizaje Progresivo** | ❌ | ❌ | ✅ | ✅ |
|
|
1513
|
+
| **PRs por feature** | 1 grande | 1 grande | 1 grande | N pequeños |
|
|
1514
|
+
| **Review time** | Largo | Largo | Largo | 30-60 min |
|
|
1515
|
+
|
|
1516
|
+
### v4 = Lo mejor de todas las versiones
|
|
1517
|
+
|
|
1518
|
+
```
|
|
1519
|
+
v4 = v1 (Kanban + XP)
|
|
1520
|
+
+ v2 (GitHub MCP automation)
|
|
1521
|
+
+ v3 (Docker first + Learning)
|
|
1522
|
+
+ NEW (GitFlow + Stacked PRs)
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
---
|
|
1526
|
+
|
|
1527
|
+
## 🔑 Beneficios de v4
|
|
1528
|
+
|
|
1529
|
+
### De v1 - Kanban Light + XP Adaptado
|
|
1530
|
+
1. **WIP Limits** - Máximo 2 tareas en progreso
|
|
1531
|
+
2. **TDD Selectivo** - Tests solo en áreas críticas
|
|
1532
|
+
3. **Refactorización planificada** - Viernes después de demo
|
|
1533
|
+
4. **Rutina semanal** - Lunes planificación, Viernes demo
|
|
1534
|
+
5. **Mínimo overhead** - Sin ceremonias innecesarias
|
|
1535
|
+
|
|
1536
|
+
### De v2 - GitHub MCP
|
|
1537
|
+
6. **Automatización total** - Repos, issues, projects creados automáticamente
|
|
1538
|
+
7. **CI/CD generado** - Workflows según tech stack
|
|
1539
|
+
8. **PR templates** - Formato consistente
|
|
1540
|
+
|
|
1541
|
+
### De v3 - Infrastructure First + Learning
|
|
1542
|
+
9. **Docker primero** - Entorno reproducible desde día 1
|
|
1543
|
+
10. **Aprendizaje progresivo** - Conceptos cuando los necesitas
|
|
1544
|
+
11. **Documentación just-in-time** - No leer 50 páginas antes
|
|
1545
|
+
|
|
1546
|
+
### Nuevos en v4 - GitFlow + Stacked PRs
|
|
1547
|
+
12. **Reviews más rápidos** - PRs pequeños = review en 30-60 min
|
|
1548
|
+
13. **Feedback continuo** - No esperar a terminar toda la feature
|
|
1549
|
+
14. **Menos conflictos** - Merge incremental reduce merge hell
|
|
1550
|
+
15. **Mejor tracking** - Issue de tracking por stack
|
|
1551
|
+
16. **Rollback granular** - Revertir un PR sin afectar todo
|
|
1552
|
+
17. **GitFlow profesional** - main/develop/feature/release/hotfix
|
|
1553
|
+
18. **Aprendizaje por PR** - Conceptos en chunks aún más pequeños
|
|
1554
|
+
|
|
1555
|
+
---
|
|
1556
|
+
|
|
1557
|
+
## 🎯 ¿Cuándo usar v4?
|
|
1558
|
+
|
|
1559
|
+
| Escenario | Recomendación |
|
|
1560
|
+
|-----------|---------------|
|
|
1561
|
+
| Proyecto nuevo con equipo de 1+ personas | ✅ v4 |
|
|
1562
|
+
| Features grandes que necesitan review incremental | ✅ v4 |
|
|
1563
|
+
| Quieres aprender mientras desarrollas | ✅ v4 |
|
|
1564
|
+
| Proyecto profesional con GitFlow | ✅ v4 |
|
|
1565
|
+
| Proyecto muy simple o prototipo rápido | ❌ Usa v1 |
|
|
1566
|
+
| Solo necesitas automatización GitHub básica | ❌ Usa v2 |
|
|
1567
|
+
|
|
1568
|
+
---
|
|
1569
|
+
|
|
1570
|
+
## 📊 Comparativa Visual
|
|
1571
|
+
|
|
1572
|
+
```
|
|
1573
|
+
┌─────────────────────────────────────────────────┐
|
|
1574
|
+
│ FREELANCE PLANNER v4 │
|
|
1575
|
+
│ "The Complete Package" │
|
|
1576
|
+
└─────────────────────────────────────────────────┘
|
|
1577
|
+
│
|
|
1578
|
+
┌──────────────────────────────┼──────────────────────────────┐
|
|
1579
|
+
│ │ │
|
|
1580
|
+
▼ ▼ ▼
|
|
1581
|
+
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
|
1582
|
+
│ 📋 KANBAN + XP │ │ 🐳 INFRASTRUCTURE │ │ 📚 STACKED PRs │
|
|
1583
|
+
│ (from v1) │ │ (from v3) │ │ (NEW in v4) │
|
|
1584
|
+
├─────────────────────┤ ├─────────────────────┤ ├─────────────────────┤
|
|
1585
|
+
│ • WIP Limits (2) │ │ • Docker first │ │ • GitFlow │
|
|
1586
|
+
│ • TDD selectivo │ │ • CI/CD priority │ │ • PRs pequeños │
|
|
1587
|
+
│ • Refactor viernes │ │ • Learning │ │ • Review rápido │
|
|
1588
|
+
│ • Demo semanal │ │ progresivo │ │ • Merge incremental │
|
|
1589
|
+
│ • Mínimo overhead │ │ • Just-in-time docs │ │ • Stack tracking │
|
|
1590
|
+
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
|
|
1591
|
+
│ │ │
|
|
1592
|
+
└──────────────────────────────┼──────────────────────────────┘
|
|
1593
|
+
│
|
|
1594
|
+
▼
|
|
1595
|
+
┌─────────────────────────────────────────────────┐
|
|
1596
|
+
│ 🔗 GITHUB MCP (from v2) │
|
|
1597
|
+
│ Auto-create: repos, issues, projects, CI/CD │
|
|
1598
|
+
└─────────────────────────────────────────────────┘
|
|
1599
|
+
```
|
|
1600
|
+
|
|
1601
|
+
---
|
|
1602
|
+
|
|
1603
|
+
## 🛠️ Tooling para Stacked PRs
|
|
1604
|
+
|
|
1605
|
+
### Herramientas Recomendadas
|
|
1606
|
+
|
|
1607
|
+
El manejo manual de stacked PRs puede ser tedioso. Estas herramientas automatizan el proceso:
|
|
1608
|
+
|
|
1609
|
+
```typescript
|
|
1610
|
+
const STACKED_PR_TOOLS = {
|
|
1611
|
+
// Herramientas especializadas
|
|
1612
|
+
graphite: {
|
|
1613
|
+
name: 'Graphite',
|
|
1614
|
+
type: 'cli + web',
|
|
1615
|
+
pros: ['Mejor UX', 'Dashboard web', 'Auto-sync', 'Merge queue'],
|
|
1616
|
+
cons: ['Requiere cuenta', 'Freemium'],
|
|
1617
|
+
recommendation: 'Mejor para uso frecuente de stacks'
|
|
1618
|
+
},
|
|
1619
|
+
|
|
1620
|
+
gitTown: {
|
|
1621
|
+
name: 'git-town',
|
|
1622
|
+
type: 'cli',
|
|
1623
|
+
pros: ['Open source', 'Sin cuenta', 'Integrado con git'],
|
|
1624
|
+
cons: ['Solo CLI', 'Menos features'],
|
|
1625
|
+
recommendation: 'Mejor para puristas de git'
|
|
1626
|
+
},
|
|
1627
|
+
|
|
1628
|
+
ghStack: {
|
|
1629
|
+
name: 'ghstack (Facebook)',
|
|
1630
|
+
type: 'cli',
|
|
1631
|
+
pros: ['Usado en producción en Meta', 'Robusto'],
|
|
1632
|
+
cons: ['Curva de aprendizaje', 'Pensado para grandes equipos'],
|
|
1633
|
+
recommendation: 'Para proyectos muy grandes'
|
|
1634
|
+
},
|
|
1635
|
+
|
|
1636
|
+
custom: {
|
|
1637
|
+
name: 'Scripts personalizados',
|
|
1638
|
+
type: 'bash/python',
|
|
1639
|
+
pros: ['Control total', 'Sin dependencias', 'Personalizable'],
|
|
1640
|
+
cons: ['Hay que mantenerlo', 'Más trabajo inicial'],
|
|
1641
|
+
recommendation: 'Cuando necesitas control total'
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
```
|
|
1645
|
+
|
|
1646
|
+
### Opción 1: Graphite (Recomendada para la mayoría)
|
|
1647
|
+
|
|
1648
|
+
```bash
|
|
1649
|
+
# Instalación
|
|
1650
|
+
npm install -g @withgraphite/graphite-cli
|
|
1651
|
+
|
|
1652
|
+
# Autenticación
|
|
1653
|
+
gt auth
|
|
1654
|
+
|
|
1655
|
+
# Flujo de trabajo con Graphite
|
|
1656
|
+
# =============================
|
|
1657
|
+
|
|
1658
|
+
# 1. Crear primer PR del stack
|
|
1659
|
+
gt create -m "01: Database schema for users"
|
|
1660
|
+
|
|
1661
|
+
# 2. Hacer cambios y crear siguiente PR (se apila automáticamente)
|
|
1662
|
+
gt create -m "02: API endpoints for auth"
|
|
1663
|
+
|
|
1664
|
+
# 3. Crear más PRs del stack
|
|
1665
|
+
gt create -m "03: JWT middleware"
|
|
1666
|
+
gt create -m "04: Frontend login forms"
|
|
1667
|
+
|
|
1668
|
+
# Ver el stack completo
|
|
1669
|
+
gt log
|
|
1670
|
+
|
|
1671
|
+
# Output:
|
|
1672
|
+
# ┌── 04: Frontend login forms (current)
|
|
1673
|
+
# ├── 03: JWT middleware
|
|
1674
|
+
# ├── 02: API endpoints for auth
|
|
1675
|
+
# └── 01: Database schema for users
|
|
1676
|
+
# │
|
|
1677
|
+
# └── main
|
|
1678
|
+
|
|
1679
|
+
# Sincronizar después de que se mergea un PR
|
|
1680
|
+
gt sync # Rebasa automáticamente todos los PRs del stack
|
|
1681
|
+
|
|
1682
|
+
# Navegar entre PRs del stack
|
|
1683
|
+
gt up # Ir al PR siguiente
|
|
1684
|
+
gt down # Ir al PR anterior
|
|
1685
|
+
gt top # Ir al último PR del stack
|
|
1686
|
+
gt bottom # Ir al primer PR del stack
|
|
1687
|
+
|
|
1688
|
+
# Submit todos los PRs a GitHub
|
|
1689
|
+
gt submit --stack
|
|
1690
|
+
```
|
|
1691
|
+
|
|
1692
|
+
### Opción 2: git-town (Open Source)
|
|
1693
|
+
|
|
1694
|
+
```bash
|
|
1695
|
+
# Instalación
|
|
1696
|
+
# macOS
|
|
1697
|
+
brew install git-town
|
|
1698
|
+
|
|
1699
|
+
# Linux
|
|
1700
|
+
curl -sL https://git-town.com/install.sh | bash
|
|
1701
|
+
|
|
1702
|
+
# Windows
|
|
1703
|
+
scoop install git-town
|
|
1704
|
+
|
|
1705
|
+
# Configuración inicial
|
|
1706
|
+
git town config
|
|
1707
|
+
|
|
1708
|
+
# Flujo de trabajo con git-town
|
|
1709
|
+
# =============================
|
|
1710
|
+
|
|
1711
|
+
# 1. Crear rama para feature
|
|
1712
|
+
git town hack feature/user-auth-01-schema
|
|
1713
|
+
|
|
1714
|
+
# 2. Hacer cambios y commit
|
|
1715
|
+
git add . && git commit -m "Add user schema"
|
|
1716
|
+
|
|
1717
|
+
# 3. Crear siguiente rama del stack
|
|
1718
|
+
git town append feature/user-auth-02-api
|
|
1719
|
+
|
|
1720
|
+
# 4. Sincronizar con upstream
|
|
1721
|
+
git town sync
|
|
1722
|
+
|
|
1723
|
+
# 5. Crear PR
|
|
1724
|
+
git town propose
|
|
1725
|
+
|
|
1726
|
+
# Ver estado
|
|
1727
|
+
git town status
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
### Opción 3: Scripts Personalizados (Agnóstico)
|
|
1731
|
+
|
|
1732
|
+
```bash
|
|
1733
|
+
#!/bin/bash
|
|
1734
|
+
# scripts/stack-create.sh
|
|
1735
|
+
# Crear un nuevo PR en el stack
|
|
1736
|
+
|
|
1737
|
+
set -e
|
|
1738
|
+
|
|
1739
|
+
FEATURE_NAME=$1
|
|
1740
|
+
DESCRIPTION=$2
|
|
1741
|
+
|
|
1742
|
+
if [ -z "$FEATURE_NAME" ] || [ -z "$DESCRIPTION" ]; then
|
|
1743
|
+
echo "Uso: ./scripts/stack-create.sh <feature-name> <description>"
|
|
1744
|
+
echo "Ejemplo: ./scripts/stack-create.sh user-auth 'Add JWT middleware'"
|
|
1745
|
+
exit 1
|
|
1746
|
+
fi
|
|
1747
|
+
|
|
1748
|
+
# Detectar el número del siguiente PR en el stack
|
|
1749
|
+
CURRENT_BRANCH=$(git branch --show-current)
|
|
1750
|
+
STACK_BASE="feature/${FEATURE_NAME}"
|
|
1751
|
+
|
|
1752
|
+
if [[ $CURRENT_BRANCH =~ ^${STACK_BASE}/([0-9]+)- ]]; then
|
|
1753
|
+
CURRENT_NUM=${BASH_REMATCH[1]}
|
|
1754
|
+
NEXT_NUM=$(printf "%02d" $((10#$CURRENT_NUM + 1)))
|
|
1755
|
+
else
|
|
1756
|
+
# Es el primer PR del stack
|
|
1757
|
+
NEXT_NUM="01"
|
|
1758
|
+
fi
|
|
1759
|
+
|
|
1760
|
+
# Crear slug del description
|
|
1761
|
+
SLUG=$(echo "$DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//')
|
|
1762
|
+
|
|
1763
|
+
NEW_BRANCH="${STACK_BASE}/${NEXT_NUM}-${SLUG}"
|
|
1764
|
+
|
|
1765
|
+
echo "📚 Creando nueva rama del stack: $NEW_BRANCH"
|
|
1766
|
+
|
|
1767
|
+
# Crear rama basada en la actual
|
|
1768
|
+
git checkout -b "$NEW_BRANCH"
|
|
1769
|
+
|
|
1770
|
+
echo "✅ Rama creada. Ahora puedes hacer tus cambios."
|
|
1771
|
+
echo ""
|
|
1772
|
+
echo "Cuando termines:"
|
|
1773
|
+
echo " git add ."
|
|
1774
|
+
echo " git commit -m '${NEXT_NUM}: ${DESCRIPTION}'"
|
|
1775
|
+
echo " ./scripts/stack-push.sh"
|
|
1776
|
+
```
|
|
1777
|
+
|
|
1778
|
+
```bash
|
|
1779
|
+
#!/bin/bash
|
|
1780
|
+
# scripts/stack-sync.sh
|
|
1781
|
+
# Sincronizar stack después de merge
|
|
1782
|
+
|
|
1783
|
+
set -e
|
|
1784
|
+
|
|
1785
|
+
FEATURE_NAME=$1
|
|
1786
|
+
|
|
1787
|
+
if [ -z "$FEATURE_NAME" ]; then
|
|
1788
|
+
# Detectar feature del branch actual
|
|
1789
|
+
CURRENT_BRANCH=$(git branch --show-current)
|
|
1790
|
+
if [[ $CURRENT_BRANCH =~ ^feature/([^/]+)/ ]]; then
|
|
1791
|
+
FEATURE_NAME=${BASH_REMATCH[1]}
|
|
1792
|
+
else
|
|
1793
|
+
echo "Uso: ./scripts/stack-sync.sh <feature-name>"
|
|
1794
|
+
exit 1
|
|
1795
|
+
fi
|
|
1796
|
+
fi
|
|
1797
|
+
|
|
1798
|
+
STACK_BASE="feature/${FEATURE_NAME}"
|
|
1799
|
+
|
|
1800
|
+
echo "🔄 Sincronizando stack: $STACK_BASE"
|
|
1801
|
+
|
|
1802
|
+
# Obtener cambios de origin
|
|
1803
|
+
git fetch origin
|
|
1804
|
+
|
|
1805
|
+
# Obtener todas las ramas del stack
|
|
1806
|
+
STACK_BRANCHES=$(git branch -r | grep "origin/${STACK_BASE}/" | sort | sed 's/origin\///')
|
|
1807
|
+
|
|
1808
|
+
# Actualizar develop primero
|
|
1809
|
+
git checkout develop
|
|
1810
|
+
git pull origin develop
|
|
1811
|
+
|
|
1812
|
+
PREV_BRANCH="develop"
|
|
1813
|
+
|
|
1814
|
+
for BRANCH in $STACK_BRANCHES; do
|
|
1815
|
+
echo "📌 Actualizando: $BRANCH"
|
|
1816
|
+
|
|
1817
|
+
git checkout "$BRANCH"
|
|
1818
|
+
|
|
1819
|
+
# Rebase sobre la rama anterior del stack (o develop si es la primera)
|
|
1820
|
+
git rebase "$PREV_BRANCH"
|
|
1821
|
+
|
|
1822
|
+
# Push con force-with-lease (seguro)
|
|
1823
|
+
git push origin "$BRANCH" --force-with-lease
|
|
1824
|
+
|
|
1825
|
+
PREV_BRANCH="$BRANCH"
|
|
1826
|
+
done
|
|
1827
|
+
|
|
1828
|
+
echo "✅ Stack sincronizado"
|
|
1829
|
+
```
|
|
1830
|
+
|
|
1831
|
+
```bash
|
|
1832
|
+
#!/bin/bash
|
|
1833
|
+
# scripts/stack-status.sh
|
|
1834
|
+
# Ver estado del stack
|
|
1835
|
+
|
|
1836
|
+
FEATURE_NAME=$1
|
|
1837
|
+
|
|
1838
|
+
if [ -z "$FEATURE_NAME" ]; then
|
|
1839
|
+
CURRENT_BRANCH=$(git branch --show-current)
|
|
1840
|
+
if [[ $CURRENT_BRANCH =~ ^feature/([^/]+)/ ]]; then
|
|
1841
|
+
FEATURE_NAME=${BASH_REMATCH[1]}
|
|
1842
|
+
else
|
|
1843
|
+
echo "Uso: ./scripts/stack-status.sh <feature-name>"
|
|
1844
|
+
exit 1
|
|
1845
|
+
fi
|
|
1846
|
+
fi
|
|
1847
|
+
|
|
1848
|
+
STACK_BASE="feature/${FEATURE_NAME}"
|
|
1849
|
+
|
|
1850
|
+
echo "📚 Stack: $STACK_BASE"
|
|
1851
|
+
echo "================================"
|
|
1852
|
+
|
|
1853
|
+
# Obtener PRs del stack
|
|
1854
|
+
gh pr list --search "head:${STACK_BASE}/" --json number,title,state,mergeable,headRefName \
|
|
1855
|
+
--jq '.[] | "PR #\(.number) [\(.state)] \(.title)"' | sort
|
|
1856
|
+
|
|
1857
|
+
echo ""
|
|
1858
|
+
echo "Branches locales:"
|
|
1859
|
+
git branch | grep "$STACK_BASE" | while read branch; do
|
|
1860
|
+
COMMITS_AHEAD=$(git rev-list --count develop.."$branch" 2>/dev/null || echo "?")
|
|
1861
|
+
echo " $branch (+$COMMITS_AHEAD commits)"
|
|
1862
|
+
done
|
|
1863
|
+
```
|
|
1864
|
+
|
|
1865
|
+
```python
|
|
1866
|
+
#!/usr/bin/env python3
|
|
1867
|
+
# scripts/stack-manager.py
|
|
1868
|
+
# Gestor de stacks multi-plataforma
|
|
1869
|
+
|
|
1870
|
+
import subprocess
|
|
1871
|
+
import sys
|
|
1872
|
+
import re
|
|
1873
|
+
import json
|
|
1874
|
+
from pathlib import Path
|
|
1875
|
+
|
|
1876
|
+
class StackManager:
|
|
1877
|
+
def __init__(self):
|
|
1878
|
+
self.current_branch = self._run_git("branch --show-current")
|
|
1879
|
+
|
|
1880
|
+
def _run_git(self, cmd: str) -> str:
|
|
1881
|
+
result = subprocess.run(
|
|
1882
|
+
f"git {cmd}",
|
|
1883
|
+
shell=True,
|
|
1884
|
+
capture_output=True,
|
|
1885
|
+
text=True
|
|
1886
|
+
)
|
|
1887
|
+
return result.stdout.strip()
|
|
1888
|
+
|
|
1889
|
+
def _run_gh(self, cmd: str) -> dict:
|
|
1890
|
+
result = subprocess.run(
|
|
1891
|
+
f"gh {cmd}",
|
|
1892
|
+
shell=True,
|
|
1893
|
+
capture_output=True,
|
|
1894
|
+
text=True
|
|
1895
|
+
)
|
|
1896
|
+
if result.returncode == 0:
|
|
1897
|
+
return json.loads(result.stdout) if result.stdout else {}
|
|
1898
|
+
return {}
|
|
1899
|
+
|
|
1900
|
+
def detect_stack(self) -> dict:
|
|
1901
|
+
"""Detecta información del stack actual"""
|
|
1902
|
+
match = re.match(r'^feature/([^/]+)/(\d+)-(.+)$', self.current_branch)
|
|
1903
|
+
if match:
|
|
1904
|
+
return {
|
|
1905
|
+
'feature': match.group(1),
|
|
1906
|
+
'position': int(match.group(2)),
|
|
1907
|
+
'slug': match.group(3),
|
|
1908
|
+
'is_stack': True
|
|
1909
|
+
}
|
|
1910
|
+
return {'is_stack': False}
|
|
1911
|
+
|
|
1912
|
+
def list_stack_branches(self, feature: str) -> list:
|
|
1913
|
+
"""Lista todas las ramas de un stack"""
|
|
1914
|
+
branches = self._run_git("branch -a")
|
|
1915
|
+
pattern = f"feature/{feature}/"
|
|
1916
|
+
stack_branches = [
|
|
1917
|
+
b.strip().replace("remotes/origin/", "")
|
|
1918
|
+
for b in branches.split("\n")
|
|
1919
|
+
if pattern in b
|
|
1920
|
+
]
|
|
1921
|
+
return sorted(set(stack_branches))
|
|
1922
|
+
|
|
1923
|
+
def get_stack_prs(self, feature: str) -> list:
|
|
1924
|
+
"""Obtiene los PRs de GitHub para el stack"""
|
|
1925
|
+
prs = self._run_gh(
|
|
1926
|
+
f'pr list --search "head:feature/{feature}/" '
|
|
1927
|
+
f'--json number,title,state,headRefName,mergeable'
|
|
1928
|
+
)
|
|
1929
|
+
return sorted(prs, key=lambda x: x.get('headRefName', ''))
|
|
1930
|
+
|
|
1931
|
+
def create_stack_branch(self, feature: str, description: str) -> str:
|
|
1932
|
+
"""Crea una nueva rama en el stack"""
|
|
1933
|
+
branches = self.list_stack_branches(feature)
|
|
1934
|
+
|
|
1935
|
+
if branches:
|
|
1936
|
+
# Encontrar el siguiente número
|
|
1937
|
+
numbers = []
|
|
1938
|
+
for b in branches:
|
|
1939
|
+
match = re.search(r'/(\d+)-', b)
|
|
1940
|
+
if match:
|
|
1941
|
+
numbers.append(int(match.group(1)))
|
|
1942
|
+
next_num = max(numbers) + 1 if numbers else 1
|
|
1943
|
+
else:
|
|
1944
|
+
next_num = 1
|
|
1945
|
+
|
|
1946
|
+
# Crear slug
|
|
1947
|
+
slug = re.sub(r'[^a-z0-9]+', '-', description.lower()).strip('-')
|
|
1948
|
+
branch_name = f"feature/{feature}/{next_num:02d}-{slug}"
|
|
1949
|
+
|
|
1950
|
+
self._run_git(f"checkout -b {branch_name}")
|
|
1951
|
+
return branch_name
|
|
1952
|
+
|
|
1953
|
+
def sync_stack(self, feature: str):
|
|
1954
|
+
"""Sincroniza todo el stack con develop"""
|
|
1955
|
+
print(f"🔄 Sincronizando stack: {feature}")
|
|
1956
|
+
|
|
1957
|
+
self._run_git("fetch origin")
|
|
1958
|
+
branches = self.list_stack_branches(feature)
|
|
1959
|
+
|
|
1960
|
+
self._run_git("checkout develop")
|
|
1961
|
+
self._run_git("pull origin develop")
|
|
1962
|
+
|
|
1963
|
+
prev_branch = "develop"
|
|
1964
|
+
for branch in branches:
|
|
1965
|
+
print(f" 📌 Rebasing: {branch}")
|
|
1966
|
+
self._run_git(f"checkout {branch}")
|
|
1967
|
+
self._run_git(f"rebase {prev_branch}")
|
|
1968
|
+
self._run_git(f"push origin {branch} --force-with-lease")
|
|
1969
|
+
prev_branch = branch
|
|
1970
|
+
|
|
1971
|
+
print("✅ Stack sincronizado")
|
|
1972
|
+
|
|
1973
|
+
def print_status(self, feature: str):
|
|
1974
|
+
"""Imprime el estado del stack"""
|
|
1975
|
+
branches = self.list_stack_branches(feature)
|
|
1976
|
+
prs = self.get_stack_prs(feature)
|
|
1977
|
+
|
|
1978
|
+
pr_map = {pr['headRefName']: pr for pr in prs}
|
|
1979
|
+
|
|
1980
|
+
print(f"\n📚 Stack: feature/{feature}")
|
|
1981
|
+
print("=" * 50)
|
|
1982
|
+
|
|
1983
|
+
for i, branch in enumerate(branches):
|
|
1984
|
+
pr = pr_map.get(branch, {})
|
|
1985
|
+
pr_num = pr.get('number', '?')
|
|
1986
|
+
state = pr.get('state', 'NO PR')
|
|
1987
|
+
|
|
1988
|
+
icon = '✅' if state == 'MERGED' else '🔄' if state == 'OPEN' else '⬜'
|
|
1989
|
+
connector = '└──' if i == len(branches) - 1 else '├──'
|
|
1990
|
+
|
|
1991
|
+
print(f" {connector} {icon} #{pr_num} {branch}")
|
|
1992
|
+
|
|
1993
|
+
print(f"\n └── develop (base)")
|
|
1994
|
+
|
|
1995
|
+
if __name__ == "__main__":
|
|
1996
|
+
manager = StackManager()
|
|
1997
|
+
|
|
1998
|
+
if len(sys.argv) < 2:
|
|
1999
|
+
print("Uso: stack-manager.py <comando> [args]")
|
|
2000
|
+
print("Comandos: status, sync, create, list")
|
|
2001
|
+
sys.exit(1)
|
|
2002
|
+
|
|
2003
|
+
command = sys.argv[1]
|
|
2004
|
+
|
|
2005
|
+
if command == "status":
|
|
2006
|
+
info = manager.detect_stack()
|
|
2007
|
+
if info['is_stack']:
|
|
2008
|
+
manager.print_status(info['feature'])
|
|
2009
|
+
else:
|
|
2010
|
+
print("No estás en una rama de stack")
|
|
2011
|
+
|
|
2012
|
+
elif command == "sync":
|
|
2013
|
+
info = manager.detect_stack()
|
|
2014
|
+
if info['is_stack']:
|
|
2015
|
+
manager.sync_stack(info['feature'])
|
|
2016
|
+
elif len(sys.argv) > 2:
|
|
2017
|
+
manager.sync_stack(sys.argv[2])
|
|
2018
|
+
else:
|
|
2019
|
+
print("Especifica el feature: stack-manager.py sync <feature>")
|
|
2020
|
+
|
|
2021
|
+
elif command == "create":
|
|
2022
|
+
if len(sys.argv) < 4:
|
|
2023
|
+
print("Uso: stack-manager.py create <feature> <description>")
|
|
2024
|
+
sys.exit(1)
|
|
2025
|
+
branch = manager.create_stack_branch(sys.argv[2], sys.argv[3])
|
|
2026
|
+
print(f"✅ Creada rama: {branch}")
|
|
2027
|
+
|
|
2028
|
+
elif command == "list":
|
|
2029
|
+
if len(sys.argv) > 2:
|
|
2030
|
+
branches = manager.list_stack_branches(sys.argv[2])
|
|
2031
|
+
for b in branches:
|
|
2032
|
+
print(f" - {b}")
|
|
2033
|
+
else:
|
|
2034
|
+
print("Especifica el feature: stack-manager.py list <feature>")
|
|
2035
|
+
```
|
|
2036
|
+
|
|
2037
|
+
---
|
|
2038
|
+
|
|
2039
|
+
## 🌐 Preview Environments por PR (Multi-Lenguaje)
|
|
2040
|
+
|
|
2041
|
+
### Detección Automática de Tech Stack
|
|
2042
|
+
|
|
2043
|
+
```typescript
|
|
2044
|
+
class TechStackDetector {
|
|
2045
|
+
/**
|
|
2046
|
+
* Detecta el tech stack del proyecto para configurar
|
|
2047
|
+
* el preview environment correcto
|
|
2048
|
+
*/
|
|
2049
|
+
async detect(projectPath: string): Promise<TechStack> {
|
|
2050
|
+
const files = await this.scanProjectFiles(projectPath);
|
|
2051
|
+
|
|
2052
|
+
return {
|
|
2053
|
+
// Lenguaje principal
|
|
2054
|
+
language: this.detectLanguage(files),
|
|
2055
|
+
|
|
2056
|
+
// Framework
|
|
2057
|
+
framework: this.detectFramework(files),
|
|
2058
|
+
|
|
2059
|
+
// Package manager
|
|
2060
|
+
packageManager: this.detectPackageManager(files),
|
|
2061
|
+
|
|
2062
|
+
// Runtime
|
|
2063
|
+
runtime: this.detectRuntime(files),
|
|
2064
|
+
|
|
2065
|
+
// Base de datos
|
|
2066
|
+
database: this.detectDatabase(files),
|
|
2067
|
+
|
|
2068
|
+
// Preview platform recomendada
|
|
2069
|
+
previewPlatform: this.recommendPreviewPlatform(files)
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
private detectLanguage(files: string[]): Language {
|
|
2074
|
+
const languageIndicators = {
|
|
2075
|
+
'package.json': 'javascript',
|
|
2076
|
+
'tsconfig.json': 'typescript',
|
|
2077
|
+
'requirements.txt': 'python',
|
|
2078
|
+
'Pipfile': 'python',
|
|
2079
|
+
'pyproject.toml': 'python',
|
|
2080
|
+
'go.mod': 'go',
|
|
2081
|
+
'Cargo.toml': 'rust',
|
|
2082
|
+
'pom.xml': 'java',
|
|
2083
|
+
'build.gradle': 'java',
|
|
2084
|
+
'Gemfile': 'ruby',
|
|
2085
|
+
'composer.json': 'php',
|
|
2086
|
+
'mix.exs': 'elixir',
|
|
2087
|
+
'Package.swift': 'swift',
|
|
2088
|
+
'pubspec.yaml': 'dart',
|
|
2089
|
+
'*.csproj': 'csharp',
|
|
2090
|
+
};
|
|
2091
|
+
|
|
2092
|
+
for (const [indicator, language] of Object.entries(languageIndicators)) {
|
|
2093
|
+
if (files.some(f => f.includes(indicator.replace('*', '')))) {
|
|
2094
|
+
return language as Language;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
return 'unknown';
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
private detectFramework(files: string[]): Framework {
|
|
2102
|
+
const frameworkIndicators = {
|
|
2103
|
+
// JavaScript/TypeScript
|
|
2104
|
+
'next.config': 'nextjs',
|
|
2105
|
+
'nuxt.config': 'nuxt',
|
|
2106
|
+
'svelte.config': 'sveltekit',
|
|
2107
|
+
'astro.config': 'astro',
|
|
2108
|
+
'remix.config': 'remix',
|
|
2109
|
+
'angular.json': 'angular',
|
|
2110
|
+
'vite.config': 'vite',
|
|
2111
|
+
|
|
2112
|
+
// Python
|
|
2113
|
+
'manage.py': 'django',
|
|
2114
|
+
'app/main.py': 'fastapi',
|
|
2115
|
+
'flask': 'flask',
|
|
2116
|
+
|
|
2117
|
+
// Go
|
|
2118
|
+
'go.mod': 'go-native',
|
|
2119
|
+
|
|
2120
|
+
// Ruby
|
|
2121
|
+
'config/routes.rb': 'rails',
|
|
2122
|
+
|
|
2123
|
+
// PHP
|
|
2124
|
+
'artisan': 'laravel',
|
|
2125
|
+
'symfony.lock': 'symfony',
|
|
2126
|
+
|
|
2127
|
+
// Java
|
|
2128
|
+
'spring': 'spring-boot',
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2131
|
+
for (const [indicator, framework] of Object.entries(frameworkIndicators)) {
|
|
2132
|
+
if (files.some(f => f.toLowerCase().includes(indicator))) {
|
|
2133
|
+
return framework as Framework;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
return 'generic';
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
private recommendPreviewPlatform(files: string[]): PreviewPlatform {
|
|
2141
|
+
const framework = this.detectFramework(files);
|
|
2142
|
+
|
|
2143
|
+
const platformMap: Record<string, PreviewPlatform> = {
|
|
2144
|
+
// Vercel - mejor para Next.js y frameworks JS
|
|
2145
|
+
'nextjs': 'vercel',
|
|
2146
|
+
'nuxt': 'vercel',
|
|
2147
|
+
'sveltekit': 'vercel',
|
|
2148
|
+
'remix': 'vercel',
|
|
2149
|
+
'astro': 'vercel',
|
|
2150
|
+
|
|
2151
|
+
// Railway - mejor para backends
|
|
2152
|
+
'django': 'railway',
|
|
2153
|
+
'fastapi': 'railway',
|
|
2154
|
+
'flask': 'railway',
|
|
2155
|
+
'rails': 'railway',
|
|
2156
|
+
'laravel': 'railway',
|
|
2157
|
+
'spring-boot': 'railway',
|
|
2158
|
+
'go-native': 'railway',
|
|
2159
|
+
|
|
2160
|
+
// Fly.io - mejor para Docker
|
|
2161
|
+
'generic': 'flyio',
|
|
2162
|
+
};
|
|
2163
|
+
|
|
2164
|
+
return platformMap[framework] || 'flyio';
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
```
|
|
2168
|
+
|
|
2169
|
+
### GitHub Actions para Preview Environments
|
|
2170
|
+
|
|
2171
|
+
```yaml
|
|
2172
|
+
# .github/workflows/preview-deploy.yml
|
|
2173
|
+
name: Deploy Preview Environment
|
|
2174
|
+
|
|
2175
|
+
on:
|
|
2176
|
+
pull_request:
|
|
2177
|
+
types: [opened, synchronize, reopened]
|
|
2178
|
+
|
|
2179
|
+
jobs:
|
|
2180
|
+
detect-stack:
|
|
2181
|
+
runs-on: ubuntu-latest
|
|
2182
|
+
outputs:
|
|
2183
|
+
language: ${{ steps.detect.outputs.language }}
|
|
2184
|
+
framework: ${{ steps.detect.outputs.framework }}
|
|
2185
|
+
platform: ${{ steps.detect.outputs.platform }}
|
|
2186
|
+
steps:
|
|
2187
|
+
- uses: actions/checkout@v4
|
|
2188
|
+
|
|
2189
|
+
- name: Detect Tech Stack
|
|
2190
|
+
id: detect
|
|
2191
|
+
run: |
|
|
2192
|
+
# Detectar lenguaje
|
|
2193
|
+
if [ -f "package.json" ]; then
|
|
2194
|
+
if [ -f "tsconfig.json" ]; then
|
|
2195
|
+
echo "language=typescript" >> $GITHUB_OUTPUT
|
|
2196
|
+
else
|
|
2197
|
+
echo "language=javascript" >> $GITHUB_OUTPUT
|
|
2198
|
+
fi
|
|
2199
|
+
elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
|
|
2200
|
+
echo "language=python" >> $GITHUB_OUTPUT
|
|
2201
|
+
elif [ -f "go.mod" ]; then
|
|
2202
|
+
echo "language=go" >> $GITHUB_OUTPUT
|
|
2203
|
+
elif [ -f "Cargo.toml" ]; then
|
|
2204
|
+
echo "language=rust" >> $GITHUB_OUTPUT
|
|
2205
|
+
elif [ -f "pom.xml" ] || [ -f "build.gradle" ]; then
|
|
2206
|
+
echo "language=java" >> $GITHUB_OUTPUT
|
|
2207
|
+
elif [ -f "Gemfile" ]; then
|
|
2208
|
+
echo "language=ruby" >> $GITHUB_OUTPUT
|
|
2209
|
+
elif [ -f "composer.json" ]; then
|
|
2210
|
+
echo "language=php" >> $GITHUB_OUTPUT
|
|
2211
|
+
else
|
|
2212
|
+
echo "language=unknown" >> $GITHUB_OUTPUT
|
|
2213
|
+
fi
|
|
2214
|
+
|
|
2215
|
+
# Detectar framework
|
|
2216
|
+
if [ -f "next.config.js" ] || [ -f "next.config.mjs" ] || [ -f "next.config.ts" ]; then
|
|
2217
|
+
echo "framework=nextjs" >> $GITHUB_OUTPUT
|
|
2218
|
+
echo "platform=vercel" >> $GITHUB_OUTPUT
|
|
2219
|
+
elif [ -f "nuxt.config.ts" ] || [ -f "nuxt.config.js" ]; then
|
|
2220
|
+
echo "framework=nuxt" >> $GITHUB_OUTPUT
|
|
2221
|
+
echo "platform=vercel" >> $GITHUB_OUTPUT
|
|
2222
|
+
elif [ -f "manage.py" ]; then
|
|
2223
|
+
echo "framework=django" >> $GITHUB_OUTPUT
|
|
2224
|
+
echo "platform=railway" >> $GITHUB_OUTPUT
|
|
2225
|
+
elif grep -q "fastapi" requirements.txt 2>/dev/null || grep -q "fastapi" pyproject.toml 2>/dev/null; then
|
|
2226
|
+
echo "framework=fastapi" >> $GITHUB_OUTPUT
|
|
2227
|
+
echo "platform=railway" >> $GITHUB_OUTPUT
|
|
2228
|
+
elif [ -f "go.mod" ]; then
|
|
2229
|
+
echo "framework=go" >> $GITHUB_OUTPUT
|
|
2230
|
+
echo "platform=flyio" >> $GITHUB_OUTPUT
|
|
2231
|
+
else
|
|
2232
|
+
echo "framework=generic" >> $GITHUB_OUTPUT
|
|
2233
|
+
echo "platform=flyio" >> $GITHUB_OUTPUT
|
|
2234
|
+
fi
|
|
2235
|
+
|
|
2236
|
+
# ==========================================
|
|
2237
|
+
# Deploy a Vercel (JS/TS Frameworks)
|
|
2238
|
+
# ==========================================
|
|
2239
|
+
deploy-vercel:
|
|
2240
|
+
needs: detect-stack
|
|
2241
|
+
if: needs.detect-stack.outputs.platform == 'vercel'
|
|
2242
|
+
runs-on: ubuntu-latest
|
|
2243
|
+
steps:
|
|
2244
|
+
- uses: actions/checkout@v4
|
|
2245
|
+
|
|
2246
|
+
- name: Deploy to Vercel
|
|
2247
|
+
id: deploy
|
|
2248
|
+
uses: amondnet/vercel-action@v25
|
|
2249
|
+
with:
|
|
2250
|
+
vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
|
2251
|
+
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
|
|
2252
|
+
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
|
|
2253
|
+
scope: ${{ secrets.VERCEL_ORG_ID }}
|
|
2254
|
+
|
|
2255
|
+
- name: Comment Preview URL
|
|
2256
|
+
uses: actions/github-script@v7
|
|
2257
|
+
with:
|
|
2258
|
+
script: |
|
|
2259
|
+
const prNumber = context.issue.number;
|
|
2260
|
+
const previewUrl = '${{ steps.deploy.outputs.preview-url }}';
|
|
2261
|
+
|
|
2262
|
+
// Detectar si es parte de un stack
|
|
2263
|
+
const branch = context.payload.pull_request.head.ref;
|
|
2264
|
+
const stackMatch = branch.match(/^feature\/([^\/]+)\/(\d+)-/);
|
|
2265
|
+
const stackInfo = stackMatch
|
|
2266
|
+
? `\n\n📚 **Stack:** \`${stackMatch[1]}\` | **Position:** #${stackMatch[2]}`
|
|
2267
|
+
: '';
|
|
2268
|
+
|
|
2269
|
+
const body = `## 🚀 Preview Deployed!
|
|
2270
|
+
|
|
2271
|
+
| Environment | URL |
|
|
2272
|
+
|-------------|-----|
|
|
2273
|
+
| **Preview** | [${previewUrl}](${previewUrl}) |
|
|
2274
|
+
${stackInfo}
|
|
2275
|
+
|
|
2276
|
+
### 🧪 Test Checklist
|
|
2277
|
+
- [ ] Funcionalidad principal verificada
|
|
2278
|
+
- [ ] Responsive design checkeado
|
|
2279
|
+
- [ ] No hay errores en consola
|
|
2280
|
+
|
|
2281
|
+
---
|
|
2282
|
+
_Deployed with Vercel • Framework: ${{ needs.detect-stack.outputs.framework }}_`;
|
|
2283
|
+
|
|
2284
|
+
github.rest.issues.createComment({
|
|
2285
|
+
issue_number: prNumber,
|
|
2286
|
+
owner: context.repo.owner,
|
|
2287
|
+
repo: context.repo.repo,
|
|
2288
|
+
body: body
|
|
2289
|
+
});
|
|
2290
|
+
|
|
2291
|
+
# ==========================================
|
|
2292
|
+
# Deploy a Railway (Backend Frameworks)
|
|
2293
|
+
# ==========================================
|
|
2294
|
+
deploy-railway:
|
|
2295
|
+
needs: detect-stack
|
|
2296
|
+
if: needs.detect-stack.outputs.platform == 'railway'
|
|
2297
|
+
runs-on: ubuntu-latest
|
|
2298
|
+
steps:
|
|
2299
|
+
- uses: actions/checkout@v4
|
|
2300
|
+
|
|
2301
|
+
- name: Install Railway CLI
|
|
2302
|
+
run: npm install -g @railway/cli
|
|
2303
|
+
|
|
2304
|
+
- name: Deploy to Railway
|
|
2305
|
+
id: deploy
|
|
2306
|
+
env:
|
|
2307
|
+
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
|
|
2308
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
2309
|
+
run: |
|
|
2310
|
+
# Deploy con nombre basado en PR
|
|
2311
|
+
PREVIEW_NAME="pr-${PR_NUMBER}"
|
|
2312
|
+
|
|
2313
|
+
railway up --detach --environment "$PREVIEW_NAME"
|
|
2314
|
+
|
|
2315
|
+
# Obtener URL del deployment
|
|
2316
|
+
PREVIEW_URL=$(railway status --json | jq -r '.deploymentUrl')
|
|
2317
|
+
echo "url=$PREVIEW_URL" >> $GITHUB_OUTPUT
|
|
2318
|
+
|
|
2319
|
+
- name: Comment Preview URL
|
|
2320
|
+
uses: actions/github-script@v7
|
|
2321
|
+
with:
|
|
2322
|
+
script: |
|
|
2323
|
+
const body = `## 🚀 Preview Deployed!
|
|
2324
|
+
|
|
2325
|
+
| Environment | URL |
|
|
2326
|
+
|-------------|-----|
|
|
2327
|
+
| **Preview** | [${{ steps.deploy.outputs.url }}](${{ steps.deploy.outputs.url }}) |
|
|
2328
|
+
|
|
2329
|
+
### 📋 API Endpoints para probar
|
|
2330
|
+
\`\`\`bash
|
|
2331
|
+
curl ${{ steps.deploy.outputs.url }}/health
|
|
2332
|
+
curl ${{ steps.deploy.outputs.url }}/api/v1/
|
|
2333
|
+
\`\`\`
|
|
2334
|
+
|
|
2335
|
+
---
|
|
2336
|
+
_Deployed with Railway • Framework: ${{ needs.detect-stack.outputs.framework }}_`;
|
|
2337
|
+
|
|
2338
|
+
github.rest.issues.createComment({
|
|
2339
|
+
issue_number: context.issue.number,
|
|
2340
|
+
owner: context.repo.owner,
|
|
2341
|
+
repo: context.repo.repo,
|
|
2342
|
+
body: body
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
# ==========================================
|
|
2346
|
+
# Deploy a Fly.io (Docker/Generic)
|
|
2347
|
+
# ==========================================
|
|
2348
|
+
deploy-flyio:
|
|
2349
|
+
needs: detect-stack
|
|
2350
|
+
if: needs.detect-stack.outputs.platform == 'flyio'
|
|
2351
|
+
runs-on: ubuntu-latest
|
|
2352
|
+
steps:
|
|
2353
|
+
- uses: actions/checkout@v4
|
|
2354
|
+
|
|
2355
|
+
- name: Setup Fly.io
|
|
2356
|
+
uses: superfly/flyctl-actions/setup-flyctl@master
|
|
2357
|
+
|
|
2358
|
+
- name: Deploy to Fly.io
|
|
2359
|
+
id: deploy
|
|
2360
|
+
env:
|
|
2361
|
+
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
|
2362
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
2363
|
+
REPO_NAME: ${{ github.event.repository.name }}
|
|
2364
|
+
run: |
|
|
2365
|
+
APP_NAME="${REPO_NAME}-pr-${PR_NUMBER}"
|
|
2366
|
+
|
|
2367
|
+
# Crear app si no existe
|
|
2368
|
+
flyctl apps create "$APP_NAME" --org personal 2>/dev/null || true
|
|
2369
|
+
|
|
2370
|
+
# Deploy
|
|
2371
|
+
flyctl deploy --app "$APP_NAME" --remote-only
|
|
2372
|
+
|
|
2373
|
+
# Obtener URL
|
|
2374
|
+
PREVIEW_URL="https://${APP_NAME}.fly.dev"
|
|
2375
|
+
echo "url=$PREVIEW_URL" >> $GITHUB_OUTPUT
|
|
2376
|
+
|
|
2377
|
+
- name: Comment Preview URL
|
|
2378
|
+
uses: actions/github-script@v7
|
|
2379
|
+
with:
|
|
2380
|
+
script: |
|
|
2381
|
+
const body = `## 🚀 Preview Deployed!
|
|
2382
|
+
|
|
2383
|
+
| Environment | URL |
|
|
2384
|
+
|-------------|-----|
|
|
2385
|
+
| **Preview** | [${{ steps.deploy.outputs.url }}](${{ steps.deploy.outputs.url }}) |
|
|
2386
|
+
|
|
2387
|
+
### 🐳 Docker Build
|
|
2388
|
+
- Image built and deployed successfully
|
|
2389
|
+
- Language: ${{ needs.detect-stack.outputs.language }}
|
|
2390
|
+
|
|
2391
|
+
---
|
|
2392
|
+
_Deployed with Fly.io_`;
|
|
2393
|
+
|
|
2394
|
+
github.rest.issues.createComment({
|
|
2395
|
+
issue_number: context.issue.number,
|
|
2396
|
+
owner: context.repo.owner,
|
|
2397
|
+
repo: context.repo.repo,
|
|
2398
|
+
body: body
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
# ==========================================
|
|
2402
|
+
# Cleanup Preview on PR Close
|
|
2403
|
+
# ==========================================
|
|
2404
|
+
cleanup-preview:
|
|
2405
|
+
if: github.event.action == 'closed'
|
|
2406
|
+
runs-on: ubuntu-latest
|
|
2407
|
+
steps:
|
|
2408
|
+
- name: Cleanup Vercel Preview
|
|
2409
|
+
if: env.VERCEL_TOKEN != ''
|
|
2410
|
+
continue-on-error: true
|
|
2411
|
+
run: |
|
|
2412
|
+
echo "Vercel previews se limpian automáticamente"
|
|
2413
|
+
|
|
2414
|
+
- name: Cleanup Railway Preview
|
|
2415
|
+
if: env.RAILWAY_TOKEN != ''
|
|
2416
|
+
continue-on-error: true
|
|
2417
|
+
env:
|
|
2418
|
+
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
|
|
2419
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
2420
|
+
run: |
|
|
2421
|
+
PREVIEW_NAME="pr-${PR_NUMBER}"
|
|
2422
|
+
railway environment delete "$PREVIEW_NAME" --yes || true
|
|
2423
|
+
|
|
2424
|
+
- name: Cleanup Fly.io Preview
|
|
2425
|
+
if: env.FLY_API_TOKEN != ''
|
|
2426
|
+
continue-on-error: true
|
|
2427
|
+
env:
|
|
2428
|
+
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
|
2429
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
2430
|
+
REPO_NAME: ${{ github.event.repository.name }}
|
|
2431
|
+
run: |
|
|
2432
|
+
APP_NAME="${REPO_NAME}-pr-${PR_NUMBER}"
|
|
2433
|
+
flyctl apps destroy "$APP_NAME" --yes || true
|
|
2434
|
+
```
|
|
2435
|
+
|
|
2436
|
+
---
|
|
2437
|
+
|
|
2438
|
+
## 🔄 Auto-Sync Después de Merge
|
|
2439
|
+
|
|
2440
|
+
### Workflow de Auto-Sincronización
|
|
2441
|
+
|
|
2442
|
+
```yaml
|
|
2443
|
+
# .github/workflows/stack-auto-sync.yml
|
|
2444
|
+
name: Stack Auto-Sync
|
|
2445
|
+
|
|
2446
|
+
on:
|
|
2447
|
+
pull_request:
|
|
2448
|
+
types: [closed]
|
|
2449
|
+
|
|
2450
|
+
jobs:
|
|
2451
|
+
sync-stack:
|
|
2452
|
+
if: github.event.pull_request.merged == true
|
|
2453
|
+
runs-on: ubuntu-latest
|
|
2454
|
+
permissions:
|
|
2455
|
+
contents: write
|
|
2456
|
+
pull-requests: write
|
|
2457
|
+
|
|
2458
|
+
steps:
|
|
2459
|
+
- uses: actions/checkout@v4
|
|
2460
|
+
with:
|
|
2461
|
+
fetch-depth: 0
|
|
2462
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
2463
|
+
|
|
2464
|
+
- name: Detect Stack and Update Next PR
|
|
2465
|
+
id: sync
|
|
2466
|
+
env:
|
|
2467
|
+
MERGED_BRANCH: ${{ github.event.pull_request.head.ref }}
|
|
2468
|
+
run: |
|
|
2469
|
+
echo "📚 Branch mergeado: $MERGED_BRANCH"
|
|
2470
|
+
|
|
2471
|
+
# Detectar si es parte de un stack
|
|
2472
|
+
if [[ $MERGED_BRANCH =~ ^feature/([^/]+)/([0-9]+)-(.+)$ ]]; then
|
|
2473
|
+
FEATURE="${BASH_REMATCH[1]}"
|
|
2474
|
+
CURRENT_NUM="${BASH_REMATCH[2]}"
|
|
2475
|
+
|
|
2476
|
+
echo "feature=$FEATURE" >> $GITHUB_OUTPUT
|
|
2477
|
+
echo "position=$CURRENT_NUM" >> $GITHUB_OUTPUT
|
|
2478
|
+
echo "is_stack=true" >> $GITHUB_OUTPUT
|
|
2479
|
+
|
|
2480
|
+
# Calcular siguiente número
|
|
2481
|
+
NEXT_NUM=$(printf "%02d" $((10#$CURRENT_NUM + 1)))
|
|
2482
|
+
echo "next_position=$NEXT_NUM" >> $GITHUB_OUTPUT
|
|
2483
|
+
|
|
2484
|
+
# Buscar siguiente rama del stack
|
|
2485
|
+
NEXT_BRANCH=$(git branch -r | grep "origin/feature/${FEATURE}/${NEXT_NUM}" | head -1 | xargs | sed 's/origin\///')
|
|
2486
|
+
|
|
2487
|
+
if [ -n "$NEXT_BRANCH" ]; then
|
|
2488
|
+
echo "next_branch=$NEXT_BRANCH" >> $GITHUB_OUTPUT
|
|
2489
|
+
echo "has_next=true" >> $GITHUB_OUTPUT
|
|
2490
|
+
echo "✅ Siguiente rama encontrada: $NEXT_BRANCH"
|
|
2491
|
+
else
|
|
2492
|
+
echo "has_next=false" >> $GITHUB_OUTPUT
|
|
2493
|
+
echo "📌 Este era el último PR del stack"
|
|
2494
|
+
fi
|
|
2495
|
+
else
|
|
2496
|
+
echo "is_stack=false" >> $GITHUB_OUTPUT
|
|
2497
|
+
echo "ℹ️ No es parte de un stack"
|
|
2498
|
+
fi
|
|
2499
|
+
|
|
2500
|
+
- name: Update Next PR Base
|
|
2501
|
+
if: steps.sync.outputs.has_next == 'true'
|
|
2502
|
+
env:
|
|
2503
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
2504
|
+
run: |
|
|
2505
|
+
NEXT_BRANCH="${{ steps.sync.outputs.next_branch }}"
|
|
2506
|
+
|
|
2507
|
+
# Encontrar el PR de la siguiente rama
|
|
2508
|
+
NEXT_PR=$(gh pr list --head "$NEXT_BRANCH" --json number --jq '.[0].number')
|
|
2509
|
+
|
|
2510
|
+
if [ -n "$NEXT_PR" ]; then
|
|
2511
|
+
echo "📌 Actualizando PR #$NEXT_PR"
|
|
2512
|
+
|
|
2513
|
+
# Cambiar base a develop
|
|
2514
|
+
gh pr edit "$NEXT_PR" --base develop
|
|
2515
|
+
|
|
2516
|
+
# Quitar draft status si lo tiene
|
|
2517
|
+
gh pr ready "$NEXT_PR" 2>/dev/null || true
|
|
2518
|
+
|
|
2519
|
+
# Agregar comentario
|
|
2520
|
+
gh pr comment "$NEXT_PR" --body "🎉 **PR anterior mergeado!**
|
|
2521
|
+
|
|
2522
|
+
Este PR ahora está listo para review.
|
|
2523
|
+
|
|
2524
|
+
📊 **Stack Progress:** ${{ steps.sync.outputs.position }}/${{ steps.sync.outputs.next_position }} completado
|
|
2525
|
+
|
|
2526
|
+
---
|
|
2527
|
+
_Auto-sync by Stack Manager_"
|
|
2528
|
+
|
|
2529
|
+
echo "✅ PR #$NEXT_PR actualizado"
|
|
2530
|
+
fi
|
|
2531
|
+
|
|
2532
|
+
- name: Update Stack Tracking Issue
|
|
2533
|
+
if: steps.sync.outputs.is_stack == 'true'
|
|
2534
|
+
env:
|
|
2535
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
2536
|
+
FEATURE: ${{ steps.sync.outputs.feature }}
|
|
2537
|
+
POSITION: ${{ steps.sync.outputs.position }}
|
|
2538
|
+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
|
2539
|
+
HAS_NEXT: ${{ steps.sync.outputs.has_next }}
|
|
2540
|
+
run: |
|
|
2541
|
+
# Buscar issue de tracking del stack
|
|
2542
|
+
TRACKING_ISSUE=$(gh issue list --search "Stack: $FEATURE in:title" --json number --jq '.[0].number')
|
|
2543
|
+
|
|
2544
|
+
if [ "$HAS_NEXT" = "true" ]; then
|
|
2545
|
+
NEXT_MSG="➡️ Siguiente PR listo para review"
|
|
2546
|
+
else
|
|
2547
|
+
NEXT_MSG="🎉 Stack completado!"
|
|
2548
|
+
fi
|
|
2549
|
+
|
|
2550
|
+
if [ -n "$TRACKING_ISSUE" ]; then
|
|
2551
|
+
# Agregar comentario de progreso
|
|
2552
|
+
gh issue comment "$TRACKING_ISSUE" --body "✅ **PR ${POSITION} mergeado**
|
|
2553
|
+
|
|
2554
|
+
Branch: \`${HEAD_REF}\`
|
|
2555
|
+
|
|
2556
|
+
${NEXT_MSG}"
|
|
2557
|
+
|
|
2558
|
+
# Si no hay siguiente PR, cerrar el tracking issue
|
|
2559
|
+
if [ "$HAS_NEXT" != "true" ]; then
|
|
2560
|
+
gh issue close "$TRACKING_ISSUE" --comment "🎉 **Stack completado!**
|
|
2561
|
+
|
|
2562
|
+
Todos los PRs han sido mergeados exitosamente."
|
|
2563
|
+
fi
|
|
2564
|
+
fi
|
|
2565
|
+
|
|
2566
|
+
# Rebase automático de PRs siguientes
|
|
2567
|
+
rebase-remaining-stack:
|
|
2568
|
+
needs: sync-stack
|
|
2569
|
+
if: needs.sync-stack.outputs.is_stack == 'true'
|
|
2570
|
+
runs-on: ubuntu-latest
|
|
2571
|
+
steps:
|
|
2572
|
+
- uses: actions/checkout@v4
|
|
2573
|
+
with:
|
|
2574
|
+
fetch-depth: 0
|
|
2575
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
2576
|
+
|
|
2577
|
+
- name: Rebase All Remaining PRs in Stack
|
|
2578
|
+
run: |
|
|
2579
|
+
FEATURE="${{ needs.sync-stack.outputs.feature }}"
|
|
2580
|
+
CURRENT_POS="${{ needs.sync-stack.outputs.position }}"
|
|
2581
|
+
|
|
2582
|
+
git fetch origin
|
|
2583
|
+
git checkout develop
|
|
2584
|
+
git pull origin develop
|
|
2585
|
+
|
|
2586
|
+
# Obtener todas las ramas restantes del stack
|
|
2587
|
+
REMAINING_BRANCHES=$(git branch -r | grep "origin/feature/${FEATURE}/" | sort | while read branch; do
|
|
2588
|
+
BRANCH_NUM=$(echo "$branch" | grep -oP '\d{2}(?=-)' || echo "00")
|
|
2589
|
+
if [ "$BRANCH_NUM" -gt "$CURRENT_POS" ]; then
|
|
2590
|
+
echo "${branch#origin/}"
|
|
2591
|
+
fi
|
|
2592
|
+
done)
|
|
2593
|
+
|
|
2594
|
+
PREV_BRANCH="develop"
|
|
2595
|
+
|
|
2596
|
+
for BRANCH in $REMAINING_BRANCHES; do
|
|
2597
|
+
echo "🔄 Rebasing: $BRANCH"
|
|
2598
|
+
|
|
2599
|
+
git checkout "$BRANCH"
|
|
2600
|
+
git rebase "$PREV_BRANCH" || {
|
|
2601
|
+
echo "⚠️ Conflicto en $BRANCH - requiere resolución manual"
|
|
2602
|
+
git rebase --abort
|
|
2603
|
+
continue
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
git push origin "$BRANCH" --force-with-lease
|
|
2607
|
+
PREV_BRANCH="$BRANCH"
|
|
2608
|
+
done
|
|
2609
|
+
|
|
2610
|
+
echo "✅ Rebase completado"
|
|
2611
|
+
```
|
|
2612
|
+
|
|
2613
|
+
---
|
|
2614
|
+
|
|
2615
|
+
## 📋 Merge Queue Configuration
|
|
2616
|
+
|
|
2617
|
+
### Setup de Merge Queue en GitHub
|
|
2618
|
+
|
|
2619
|
+
```typescript
|
|
2620
|
+
class MergeQueueSetup {
|
|
2621
|
+
/**
|
|
2622
|
+
* Configura merge queue para el repositorio
|
|
2623
|
+
* Requiere GitHub Enterprise o repo público
|
|
2624
|
+
*/
|
|
2625
|
+
async setupMergeQueue(repo: Repository): Promise<void> {
|
|
2626
|
+
// Habilitar merge queue en branch protection
|
|
2627
|
+
await this.githubMCP.updateBranchProtection(repo, 'develop', {
|
|
2628
|
+
required_status_checks: {
|
|
2629
|
+
strict: true,
|
|
2630
|
+
contexts: ['ci/tests', 'ci/lint', 'ci/type-check']
|
|
2631
|
+
},
|
|
2632
|
+
enforce_admins: false,
|
|
2633
|
+
required_pull_request_reviews: {
|
|
2634
|
+
required_approving_review_count: 1
|
|
2635
|
+
},
|
|
2636
|
+
// Merge Queue configuration
|
|
2637
|
+
merge_queue: {
|
|
2638
|
+
enabled: true,
|
|
2639
|
+
merge_method: 'squash',
|
|
2640
|
+
// Agrupar hasta 5 PRs para merge
|
|
2641
|
+
batch_size: 5,
|
|
2642
|
+
// Esperar 5 minutos antes de procesar
|
|
2643
|
+
wait_time_minutes: 5,
|
|
2644
|
+
// Requerir que CI pase en el batch
|
|
2645
|
+
require_branch_update: true
|
|
2646
|
+
}
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
```
|
|
2651
|
+
|
|
2652
|
+
### Workflow para Merge Queue
|
|
2653
|
+
|
|
2654
|
+
```yaml
|
|
2655
|
+
# .github/workflows/merge-queue.yml
|
|
2656
|
+
name: Merge Queue CI
|
|
2657
|
+
|
|
2658
|
+
on:
|
|
2659
|
+
merge_group:
|
|
2660
|
+
types: [checks_requested]
|
|
2661
|
+
|
|
2662
|
+
jobs:
|
|
2663
|
+
# Este job corre cuando un PR entra al merge queue
|
|
2664
|
+
merge-queue-tests:
|
|
2665
|
+
runs-on: ubuntu-latest
|
|
2666
|
+
steps:
|
|
2667
|
+
- uses: actions/checkout@v4
|
|
2668
|
+
|
|
2669
|
+
- name: Detect Language and Setup
|
|
2670
|
+
id: setup
|
|
2671
|
+
run: |
|
|
2672
|
+
if [ -f "package.json" ]; then
|
|
2673
|
+
echo "runtime=node" >> $GITHUB_OUTPUT
|
|
2674
|
+
elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
|
|
2675
|
+
echo "runtime=python" >> $GITHUB_OUTPUT
|
|
2676
|
+
elif [ -f "go.mod" ]; then
|
|
2677
|
+
echo "runtime=go" >> $GITHUB_OUTPUT
|
|
2678
|
+
elif [ -f "Cargo.toml" ]; then
|
|
2679
|
+
echo "runtime=rust" >> $GITHUB_OUTPUT
|
|
2680
|
+
fi
|
|
2681
|
+
|
|
2682
|
+
# Node.js
|
|
2683
|
+
- name: Setup Node.js
|
|
2684
|
+
if: steps.setup.outputs.runtime == 'node'
|
|
2685
|
+
uses: actions/setup-node@v4
|
|
2686
|
+
with:
|
|
2687
|
+
node-version: '20'
|
|
2688
|
+
cache: 'npm'
|
|
2689
|
+
|
|
2690
|
+
- name: Node.js Tests
|
|
2691
|
+
if: steps.setup.outputs.runtime == 'node'
|
|
2692
|
+
run: |
|
|
2693
|
+
npm ci
|
|
2694
|
+
npm run lint
|
|
2695
|
+
npm run type-check || true
|
|
2696
|
+
npm run test
|
|
2697
|
+
npm run build
|
|
2698
|
+
|
|
2699
|
+
# Python
|
|
2700
|
+
- name: Setup Python
|
|
2701
|
+
if: steps.setup.outputs.runtime == 'python'
|
|
2702
|
+
uses: actions/setup-python@v5
|
|
2703
|
+
with:
|
|
2704
|
+
python-version: '3.11'
|
|
2705
|
+
cache: 'pip'
|
|
2706
|
+
|
|
2707
|
+
- name: Python Tests
|
|
2708
|
+
if: steps.setup.outputs.runtime == 'python'
|
|
2709
|
+
run: |
|
|
2710
|
+
pip install -r requirements.txt
|
|
2711
|
+
pip install pytest flake8
|
|
2712
|
+
flake8 . --max-line-length=100 || true
|
|
2713
|
+
pytest
|
|
2714
|
+
|
|
2715
|
+
# Go
|
|
2716
|
+
- name: Setup Go
|
|
2717
|
+
if: steps.setup.outputs.runtime == 'go'
|
|
2718
|
+
uses: actions/setup-go@v5
|
|
2719
|
+
with:
|
|
2720
|
+
go-version: '1.21'
|
|
2721
|
+
|
|
2722
|
+
- name: Go Tests
|
|
2723
|
+
if: steps.setup.outputs.runtime == 'go'
|
|
2724
|
+
run: |
|
|
2725
|
+
go mod download
|
|
2726
|
+
go vet ./...
|
|
2727
|
+
go test -race ./...
|
|
2728
|
+
go build ./...
|
|
2729
|
+
|
|
2730
|
+
# Rust
|
|
2731
|
+
- name: Setup Rust
|
|
2732
|
+
if: steps.setup.outputs.runtime == 'rust'
|
|
2733
|
+
uses: dtolnay/rust-toolchain@stable
|
|
2734
|
+
|
|
2735
|
+
- name: Rust Tests
|
|
2736
|
+
if: steps.setup.outputs.runtime == 'rust'
|
|
2737
|
+
run: |
|
|
2738
|
+
cargo clippy -- -D warnings
|
|
2739
|
+
cargo test
|
|
2740
|
+
cargo build --release
|
|
2741
|
+
```
|
|
2742
|
+
|
|
2743
|
+
---
|
|
2744
|
+
|
|
2745
|
+
## 👥 CODEOWNERS Templates (Multi-Lenguaje)
|
|
2746
|
+
|
|
2747
|
+
### Generador de CODEOWNERS
|
|
2748
|
+
|
|
2749
|
+
```typescript
|
|
2750
|
+
class CodeownersGenerator {
|
|
2751
|
+
/**
|
|
2752
|
+
* Genera archivo CODEOWNERS basado en la estructura del proyecto
|
|
2753
|
+
*/
|
|
2754
|
+
async generate(projectPath: string): Promise<string> {
|
|
2755
|
+
const structure = await this.analyzeProjectStructure(projectPath);
|
|
2756
|
+
const techStack = await this.detectTechStack(projectPath);
|
|
2757
|
+
|
|
2758
|
+
let codeowners = `# ==========================================
|
|
2759
|
+
# CODEOWNERS - Auto-generated
|
|
2760
|
+
# ==========================================
|
|
2761
|
+
# Este archivo asigna reviewers automáticamente
|
|
2762
|
+
# basado en las áreas del código modificadas.
|
|
2763
|
+
#
|
|
2764
|
+
# Formato: <pattern> <@owner1> <@owner2>
|
|
2765
|
+
# Más específico = mayor prioridad
|
|
2766
|
+
# ==========================================
|
|
2767
|
+
|
|
2768
|
+
# ==========================================
|
|
2769
|
+
# Default - Todo el repositorio
|
|
2770
|
+
# ==========================================
|
|
2771
|
+
* @${structure.defaultOwner || 'tu-usuario'}
|
|
2772
|
+
|
|
2773
|
+
`;
|
|
2774
|
+
|
|
2775
|
+
// Agregar secciones según el stack detectado
|
|
2776
|
+
codeowners += this.generateSectionsByStack(techStack, structure);
|
|
2777
|
+
|
|
2778
|
+
return codeowners;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
private generateSectionsByStack(stack: TechStack, structure: ProjectStructure): string {
|
|
2782
|
+
let sections = '';
|
|
2783
|
+
|
|
2784
|
+
// Sección de infraestructura (común a todos)
|
|
2785
|
+
sections += `# ==========================================
|
|
2786
|
+
# Infrastructure & DevOps
|
|
2787
|
+
# ==========================================
|
|
2788
|
+
/.github/ @${structure.devopsOwner || structure.defaultOwner}
|
|
2789
|
+
/docker/ @${structure.devopsOwner || structure.defaultOwner}
|
|
2790
|
+
/Dockerfile* @${structure.devopsOwner || structure.defaultOwner}
|
|
2791
|
+
/docker-compose* @${structure.devopsOwner || structure.defaultOwner}
|
|
2792
|
+
/.env* @${structure.devopsOwner || structure.defaultOwner}
|
|
2793
|
+
/Makefile @${structure.devopsOwner || structure.defaultOwner}
|
|
2794
|
+
/scripts/ @${structure.devopsOwner || structure.defaultOwner}
|
|
2795
|
+
|
|
2796
|
+
`;
|
|
2797
|
+
|
|
2798
|
+
// Secciones específicas por lenguaje
|
|
2799
|
+
switch (stack.language) {
|
|
2800
|
+
case 'javascript':
|
|
2801
|
+
case 'typescript':
|
|
2802
|
+
sections += this.generateJavaScriptSections(structure);
|
|
2803
|
+
break;
|
|
2804
|
+
case 'python':
|
|
2805
|
+
sections += this.generatePythonSections(structure);
|
|
2806
|
+
break;
|
|
2807
|
+
case 'go':
|
|
2808
|
+
sections += this.generateGoSections(structure);
|
|
2809
|
+
break;
|
|
2810
|
+
case 'rust':
|
|
2811
|
+
sections += this.generateRustSections(structure);
|
|
2812
|
+
break;
|
|
2813
|
+
case 'java':
|
|
2814
|
+
sections += this.generateJavaSections(structure);
|
|
2815
|
+
break;
|
|
2816
|
+
case 'ruby':
|
|
2817
|
+
sections += this.generateRubySections(structure);
|
|
2818
|
+
break;
|
|
2819
|
+
case 'php':
|
|
2820
|
+
sections += this.generatePHPSections(structure);
|
|
2821
|
+
break;
|
|
2822
|
+
default:
|
|
2823
|
+
sections += this.generateGenericSections(structure);
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
return sections;
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
private generateJavaScriptSections(structure: ProjectStructure): string {
|
|
2830
|
+
return `# ==========================================
|
|
2831
|
+
# JavaScript/TypeScript Project
|
|
2832
|
+
# ==========================================
|
|
2833
|
+
|
|
2834
|
+
# Frontend
|
|
2835
|
+
/src/components/ @${structure.frontendOwner || structure.defaultOwner}
|
|
2836
|
+
/src/pages/ @${structure.frontendOwner || structure.defaultOwner}
|
|
2837
|
+
/src/app/ @${structure.frontendOwner || structure.defaultOwner}
|
|
2838
|
+
/src/views/ @${structure.frontendOwner || structure.defaultOwner}
|
|
2839
|
+
/src/hooks/ @${structure.frontendOwner || structure.defaultOwner}
|
|
2840
|
+
/src/styles/ @${structure.frontendOwner || structure.defaultOwner}
|
|
2841
|
+
/public/ @${structure.frontendOwner || structure.defaultOwner}
|
|
2842
|
+
|
|
2843
|
+
# Backend / API
|
|
2844
|
+
/src/api/ @${structure.backendOwner || structure.defaultOwner}
|
|
2845
|
+
/src/server/ @${structure.backendOwner || structure.defaultOwner}
|
|
2846
|
+
/src/routes/ @${structure.backendOwner || structure.defaultOwner}
|
|
2847
|
+
/src/controllers/ @${structure.backendOwner || structure.defaultOwner}
|
|
2848
|
+
/src/services/ @${structure.backendOwner || structure.defaultOwner}
|
|
2849
|
+
/src/middleware/ @${structure.backendOwner || structure.defaultOwner}
|
|
2850
|
+
|
|
2851
|
+
# Database
|
|
2852
|
+
/src/models/ @${structure.backendOwner || structure.defaultOwner}
|
|
2853
|
+
/src/db/ @${structure.backendOwner || structure.defaultOwner}
|
|
2854
|
+
/prisma/ @${structure.backendOwner || structure.defaultOwner}
|
|
2855
|
+
/drizzle/ @${structure.backendOwner || structure.defaultOwner}
|
|
2856
|
+
/migrations/ @${structure.backendOwner || structure.defaultOwner}
|
|
2857
|
+
|
|
2858
|
+
# Shared / Utils
|
|
2859
|
+
/src/lib/ @${structure.defaultOwner}
|
|
2860
|
+
/src/utils/ @${structure.defaultOwner}
|
|
2861
|
+
/src/types/ @${structure.defaultOwner}
|
|
2862
|
+
|
|
2863
|
+
# Tests
|
|
2864
|
+
/__tests__/ @${structure.defaultOwner}
|
|
2865
|
+
/tests/ @${structure.defaultOwner}
|
|
2866
|
+
*.test.ts @${structure.defaultOwner}
|
|
2867
|
+
*.test.tsx @${structure.defaultOwner}
|
|
2868
|
+
*.spec.ts @${structure.defaultOwner}
|
|
2869
|
+
|
|
2870
|
+
# Config
|
|
2871
|
+
/package.json @${structure.defaultOwner}
|
|
2872
|
+
/tsconfig.json @${structure.defaultOwner}
|
|
2873
|
+
/*.config.js @${structure.defaultOwner}
|
|
2874
|
+
/*.config.ts @${structure.defaultOwner}
|
|
2875
|
+
/*.config.mjs @${structure.defaultOwner}
|
|
2876
|
+
|
|
2877
|
+
`;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
private generatePythonSections(structure: ProjectStructure): string {
|
|
2881
|
+
return `# ==========================================
|
|
2882
|
+
# Python Project
|
|
2883
|
+
# ==========================================
|
|
2884
|
+
|
|
2885
|
+
# API / Web
|
|
2886
|
+
/app/ @${structure.backendOwner || structure.defaultOwner}
|
|
2887
|
+
/api/ @${structure.backendOwner || structure.defaultOwner}
|
|
2888
|
+
/views/ @${structure.backendOwner || structure.defaultOwner}
|
|
2889
|
+
/routes/ @${structure.backendOwner || structure.defaultOwner}
|
|
2890
|
+
|
|
2891
|
+
# Models / Database
|
|
2892
|
+
/models/ @${structure.backendOwner || structure.defaultOwner}
|
|
2893
|
+
/migrations/ @${structure.backendOwner || structure.defaultOwner}
|
|
2894
|
+
/alembic/ @${structure.backendOwner || structure.defaultOwner}
|
|
2895
|
+
|
|
2896
|
+
# Services / Business Logic
|
|
2897
|
+
/services/ @${structure.backendOwner || structure.defaultOwner}
|
|
2898
|
+
/domain/ @${structure.backendOwner || structure.defaultOwner}
|
|
2899
|
+
/core/ @${structure.backendOwner || structure.defaultOwner}
|
|
2900
|
+
|
|
2901
|
+
# Utils
|
|
2902
|
+
/utils/ @${structure.defaultOwner}
|
|
2903
|
+
/lib/ @${structure.defaultOwner}
|
|
2904
|
+
/helpers/ @${structure.defaultOwner}
|
|
2905
|
+
|
|
2906
|
+
# Tests
|
|
2907
|
+
/tests/ @${structure.defaultOwner}
|
|
2908
|
+
test_*.py @${structure.defaultOwner}
|
|
2909
|
+
*_test.py @${structure.defaultOwner}
|
|
2910
|
+
|
|
2911
|
+
# Config
|
|
2912
|
+
/requirements*.txt @${structure.defaultOwner}
|
|
2913
|
+
/pyproject.toml @${structure.defaultOwner}
|
|
2914
|
+
/setup.py @${structure.defaultOwner}
|
|
2915
|
+
/Pipfile* @${structure.defaultOwner}
|
|
2916
|
+
|
|
2917
|
+
# Django specific
|
|
2918
|
+
/templates/ @${structure.frontendOwner || structure.defaultOwner}
|
|
2919
|
+
/static/ @${structure.frontendOwner || structure.defaultOwner}
|
|
2920
|
+
/manage.py @${structure.backendOwner || structure.defaultOwner}
|
|
2921
|
+
|
|
2922
|
+
`;
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
private generateGoSections(structure: ProjectStructure): string {
|
|
2926
|
+
return `# ==========================================
|
|
2927
|
+
# Go Project
|
|
2928
|
+
# ==========================================
|
|
2929
|
+
|
|
2930
|
+
# API / Handlers
|
|
2931
|
+
/api/ @${structure.backendOwner || structure.defaultOwner}
|
|
2932
|
+
/handlers/ @${structure.backendOwner || structure.defaultOwner}
|
|
2933
|
+
/routes/ @${structure.backendOwner || structure.defaultOwner}
|
|
2934
|
+
|
|
2935
|
+
# Internal packages
|
|
2936
|
+
/internal/ @${structure.backendOwner || structure.defaultOwner}
|
|
2937
|
+
/pkg/ @${structure.defaultOwner}
|
|
2938
|
+
|
|
2939
|
+
# Commands / CLI
|
|
2940
|
+
/cmd/ @${structure.backendOwner || structure.defaultOwner}
|
|
2941
|
+
|
|
2942
|
+
# Models / Database
|
|
2943
|
+
/models/ @${structure.backendOwner || structure.defaultOwner}
|
|
2944
|
+
/repository/ @${structure.backendOwner || structure.defaultOwner}
|
|
2945
|
+
/store/ @${structure.backendOwner || structure.defaultOwner}
|
|
2946
|
+
|
|
2947
|
+
# Services
|
|
2948
|
+
/services/ @${structure.backendOwner || structure.defaultOwner}
|
|
2949
|
+
/domain/ @${structure.backendOwner || structure.defaultOwner}
|
|
2950
|
+
|
|
2951
|
+
# Tests
|
|
2952
|
+
*_test.go @${structure.defaultOwner}
|
|
2953
|
+
|
|
2954
|
+
# Config
|
|
2955
|
+
/go.mod @${structure.defaultOwner}
|
|
2956
|
+
/go.sum @${structure.defaultOwner}
|
|
2957
|
+
|
|
2958
|
+
`;
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
private generateRustSections(structure: ProjectStructure): string {
|
|
2962
|
+
return `# ==========================================
|
|
2963
|
+
# Rust Project
|
|
2964
|
+
# ==========================================
|
|
2965
|
+
|
|
2966
|
+
# Source code
|
|
2967
|
+
/src/ @${structure.defaultOwner}
|
|
2968
|
+
/src/bin/ @${structure.backendOwner || structure.defaultOwner}
|
|
2969
|
+
/src/lib.rs @${structure.defaultOwner}
|
|
2970
|
+
/src/main.rs @${structure.backendOwner || structure.defaultOwner}
|
|
2971
|
+
|
|
2972
|
+
# API / Web
|
|
2973
|
+
/src/api/ @${structure.backendOwner || structure.defaultOwner}
|
|
2974
|
+
/src/handlers/ @${structure.backendOwner || structure.defaultOwner}
|
|
2975
|
+
/src/routes/ @${structure.backendOwner || structure.defaultOwner}
|
|
2976
|
+
|
|
2977
|
+
# Domain
|
|
2978
|
+
/src/models/ @${structure.backendOwner || structure.defaultOwner}
|
|
2979
|
+
/src/domain/ @${structure.backendOwner || structure.defaultOwner}
|
|
2980
|
+
/src/services/ @${structure.backendOwner || structure.defaultOwner}
|
|
2981
|
+
|
|
2982
|
+
# Tests
|
|
2983
|
+
/tests/ @${structure.defaultOwner}
|
|
2984
|
+
|
|
2985
|
+
# Config
|
|
2986
|
+
/Cargo.toml @${structure.defaultOwner}
|
|
2987
|
+
/Cargo.lock @${structure.defaultOwner}
|
|
2988
|
+
|
|
2989
|
+
`;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
private generateJavaSections(structure: ProjectStructure): string {
|
|
2993
|
+
return `# ==========================================
|
|
2994
|
+
# Java Project
|
|
2995
|
+
# ==========================================
|
|
2996
|
+
|
|
2997
|
+
# Controllers / API
|
|
2998
|
+
**/controller/ @${structure.backendOwner || structure.defaultOwner}
|
|
2999
|
+
**/controllers/ @${structure.backendOwner || structure.defaultOwner}
|
|
3000
|
+
**/api/ @${structure.backendOwner || structure.defaultOwner}
|
|
3001
|
+
|
|
3002
|
+
# Services
|
|
3003
|
+
**/service/ @${structure.backendOwner || structure.defaultOwner}
|
|
3004
|
+
**/services/ @${structure.backendOwner || structure.defaultOwner}
|
|
3005
|
+
|
|
3006
|
+
# Models / Entities
|
|
3007
|
+
**/model/ @${structure.backendOwner || structure.defaultOwner}
|
|
3008
|
+
**/models/ @${structure.backendOwner || structure.defaultOwner}
|
|
3009
|
+
**/entity/ @${structure.backendOwner || structure.defaultOwner}
|
|
3010
|
+
**/entities/ @${structure.backendOwner || structure.defaultOwner}
|
|
3011
|
+
|
|
3012
|
+
# Repository / DAO
|
|
3013
|
+
**/repository/ @${structure.backendOwner || structure.defaultOwner}
|
|
3014
|
+
**/repositories/ @${structure.backendOwner || structure.defaultOwner}
|
|
3015
|
+
**/dao/ @${structure.backendOwner || structure.defaultOwner}
|
|
3016
|
+
|
|
3017
|
+
# Config
|
|
3018
|
+
**/config/ @${structure.devopsOwner || structure.defaultOwner}
|
|
3019
|
+
**/configuration/ @${structure.devopsOwner || structure.defaultOwner}
|
|
3020
|
+
|
|
3021
|
+
# Tests
|
|
3022
|
+
**/test/ @${structure.defaultOwner}
|
|
3023
|
+
*Test.java @${structure.defaultOwner}
|
|
3024
|
+
|
|
3025
|
+
# Build
|
|
3026
|
+
/pom.xml @${structure.defaultOwner}
|
|
3027
|
+
/build.gradle @${structure.defaultOwner}
|
|
3028
|
+
/settings.gradle @${structure.defaultOwner}
|
|
3029
|
+
|
|
3030
|
+
`;
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
private generateRubySections(structure: ProjectStructure): string {
|
|
3034
|
+
return `# ==========================================
|
|
3035
|
+
# Ruby Project
|
|
3036
|
+
# ==========================================
|
|
3037
|
+
|
|
3038
|
+
# Controllers
|
|
3039
|
+
/app/controllers/ @${structure.backendOwner || structure.defaultOwner}
|
|
3040
|
+
|
|
3041
|
+
# Models
|
|
3042
|
+
/app/models/ @${structure.backendOwner || structure.defaultOwner}
|
|
3043
|
+
|
|
3044
|
+
# Views
|
|
3045
|
+
/app/views/ @${structure.frontendOwner || structure.defaultOwner}
|
|
3046
|
+
|
|
3047
|
+
# Services / Jobs
|
|
3048
|
+
/app/services/ @${structure.backendOwner || structure.defaultOwner}
|
|
3049
|
+
/app/jobs/ @${structure.backendOwner || structure.defaultOwner}
|
|
3050
|
+
|
|
3051
|
+
# Database
|
|
3052
|
+
/db/ @${structure.backendOwner || structure.defaultOwner}
|
|
3053
|
+
/db/migrate/ @${structure.backendOwner || structure.defaultOwner}
|
|
3054
|
+
|
|
3055
|
+
# Config
|
|
3056
|
+
/config/ @${structure.devopsOwner || structure.defaultOwner}
|
|
3057
|
+
|
|
3058
|
+
# Tests
|
|
3059
|
+
/spec/ @${structure.defaultOwner}
|
|
3060
|
+
/test/ @${structure.defaultOwner}
|
|
3061
|
+
|
|
3062
|
+
# Gems
|
|
3063
|
+
/Gemfile @${structure.defaultOwner}
|
|
3064
|
+
/Gemfile.lock @${structure.defaultOwner}
|
|
3065
|
+
|
|
3066
|
+
`;
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
private generatePHPSections(structure: ProjectStructure): string {
|
|
3070
|
+
return `# ==========================================
|
|
3071
|
+
# PHP Project
|
|
3072
|
+
# ==========================================
|
|
3073
|
+
|
|
3074
|
+
# Laravel / Symfony Controllers
|
|
3075
|
+
/app/Http/Controllers/ @${structure.backendOwner || structure.defaultOwner}
|
|
3076
|
+
/src/Controller/ @${structure.backendOwner || structure.defaultOwner}
|
|
3077
|
+
|
|
3078
|
+
# Models
|
|
3079
|
+
/app/Models/ @${structure.backendOwner || structure.defaultOwner}
|
|
3080
|
+
/src/Entity/ @${structure.backendOwner || structure.defaultOwner}
|
|
3081
|
+
|
|
3082
|
+
# Views / Templates
|
|
3083
|
+
/resources/views/ @${structure.frontendOwner || structure.defaultOwner}
|
|
3084
|
+
/templates/ @${structure.frontendOwner || structure.defaultOwner}
|
|
3085
|
+
|
|
3086
|
+
# Services
|
|
3087
|
+
/app/Services/ @${structure.backendOwner || structure.defaultOwner}
|
|
3088
|
+
/src/Service/ @${structure.backendOwner || structure.defaultOwner}
|
|
3089
|
+
|
|
3090
|
+
# Database
|
|
3091
|
+
/database/ @${structure.backendOwner || structure.defaultOwner}
|
|
3092
|
+
/migrations/ @${structure.backendOwner || structure.defaultOwner}
|
|
3093
|
+
|
|
3094
|
+
# Config
|
|
3095
|
+
/config/ @${structure.devopsOwner || structure.defaultOwner}
|
|
3096
|
+
|
|
3097
|
+
# Tests
|
|
3098
|
+
/tests/ @${structure.defaultOwner}
|
|
3099
|
+
|
|
3100
|
+
# Composer
|
|
3101
|
+
/composer.json @${structure.defaultOwner}
|
|
3102
|
+
/composer.lock @${structure.defaultOwner}
|
|
3103
|
+
|
|
3104
|
+
`;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
private generateGenericSections(structure: ProjectStructure): string {
|
|
3108
|
+
return `# ==========================================
|
|
3109
|
+
# Generic Project Structure
|
|
3110
|
+
# ==========================================
|
|
3111
|
+
|
|
3112
|
+
# Source code
|
|
3113
|
+
/src/ @${structure.defaultOwner}
|
|
3114
|
+
|
|
3115
|
+
# API
|
|
3116
|
+
/api/ @${structure.backendOwner || structure.defaultOwner}
|
|
3117
|
+
|
|
3118
|
+
# Config
|
|
3119
|
+
/config/ @${structure.devopsOwner || structure.defaultOwner}
|
|
3120
|
+
|
|
3121
|
+
# Tests
|
|
3122
|
+
/tests/ @${structure.defaultOwner}
|
|
3123
|
+
/test/ @${structure.defaultOwner}
|
|
3124
|
+
|
|
3125
|
+
# Documentation
|
|
3126
|
+
/docs/ @${structure.defaultOwner}
|
|
3127
|
+
/*.md @${structure.defaultOwner}
|
|
3128
|
+
|
|
3129
|
+
`;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
```
|
|
3133
|
+
|
|
3134
|
+
### Template de CODEOWNERS para Freelancers
|
|
3135
|
+
|
|
3136
|
+
```bash
|
|
3137
|
+
# .github/CODEOWNERS
|
|
3138
|
+
# ==========================================
|
|
3139
|
+
# CODEOWNERS para Proyecto Freelance
|
|
3140
|
+
# ==========================================
|
|
3141
|
+
# Como freelancer, tú eres el owner de todo.
|
|
3142
|
+
# Este archivo sirve para:
|
|
3143
|
+
# 1. Auto-asignarte como reviewer
|
|
3144
|
+
# 2. Documentar la estructura del proyecto
|
|
3145
|
+
# 3. Facilitar onboarding si agregas colaboradores
|
|
3146
|
+
# ==========================================
|
|
3147
|
+
|
|
3148
|
+
# Default - Todo el repositorio
|
|
3149
|
+
* @tu-usuario-github
|
|
3150
|
+
|
|
3151
|
+
# ==========================================
|
|
3152
|
+
# Infrastructure
|
|
3153
|
+
# ==========================================
|
|
3154
|
+
/.github/ @tu-usuario-github
|
|
3155
|
+
/docker/ @tu-usuario-github
|
|
3156
|
+
/Dockerfile* @tu-usuario-github
|
|
3157
|
+
/docker-compose* @tu-usuario-github
|
|
3158
|
+
/scripts/ @tu-usuario-github
|
|
3159
|
+
|
|
3160
|
+
# ==========================================
|
|
3161
|
+
# Configuración Crítica (requiere atención extra)
|
|
3162
|
+
# ==========================================
|
|
3163
|
+
/.env* @tu-usuario-github
|
|
3164
|
+
/**/secrets* @tu-usuario-github
|
|
3165
|
+
/**/credentials* @tu-usuario-github
|
|
3166
|
+
|
|
3167
|
+
# ==========================================
|
|
3168
|
+
# Adapta según tu stack (descomenta lo que aplique)
|
|
3169
|
+
# ==========================================
|
|
3170
|
+
|
|
3171
|
+
# --- JavaScript/TypeScript ---
|
|
3172
|
+
# /src/components/ @tu-usuario-github
|
|
3173
|
+
# /src/pages/ @tu-usuario-github
|
|
3174
|
+
# /src/api/ @tu-usuario-github
|
|
3175
|
+
# /prisma/ @tu-usuario-github
|
|
3176
|
+
|
|
3177
|
+
# --- Python ---
|
|
3178
|
+
# /app/ @tu-usuario-github
|
|
3179
|
+
# /api/ @tu-usuario-github
|
|
3180
|
+
# /migrations/ @tu-usuario-github
|
|
3181
|
+
|
|
3182
|
+
# --- Go ---
|
|
3183
|
+
# /cmd/ @tu-usuario-github
|
|
3184
|
+
# /internal/ @tu-usuario-github
|
|
3185
|
+
# /pkg/ @tu-usuario-github
|
|
3186
|
+
|
|
3187
|
+
# ==========================================
|
|
3188
|
+
# Tests (siempre revisar con cuidado)
|
|
3189
|
+
# ==========================================
|
|
3190
|
+
/tests/ @tu-usuario-github
|
|
3191
|
+
/__tests__/ @tu-usuario-github
|
|
3192
|
+
*_test.* @tu-usuario-github
|
|
3193
|
+
*.test.* @tu-usuario-github
|
|
3194
|
+
*.spec.* @tu-usuario-github
|
|
3195
|
+
```
|
|
3196
|
+
|
|
3197
|
+
---
|
|
3198
|
+
|
|
3199
|
+
## 📝 Changelog Automático por Stack
|
|
3200
|
+
|
|
3201
|
+
### Generador de Changelog
|
|
3202
|
+
|
|
3203
|
+
```typescript
|
|
3204
|
+
class StackChangelogGenerator {
|
|
3205
|
+
/**
|
|
3206
|
+
* Genera changelog automático basado en PRs mergeados del stack
|
|
3207
|
+
*/
|
|
3208
|
+
async generateChangelog(
|
|
3209
|
+
repo: Repository,
|
|
3210
|
+
stack: PRStack
|
|
3211
|
+
): Promise<string> {
|
|
3212
|
+
const mergedPRs = stack.prStack.filter(pr => pr.merged);
|
|
3213
|
+
|
|
3214
|
+
const entries = await Promise.all(
|
|
3215
|
+
mergedPRs.map(async pr => ({
|
|
3216
|
+
type: this.detectChangeType(pr),
|
|
3217
|
+
scope: this.detectScope(pr),
|
|
3218
|
+
description: this.cleanTitle(pr.title),
|
|
3219
|
+
prNumber: pr.number,
|
|
3220
|
+
breaking: this.isBreakingChange(pr),
|
|
3221
|
+
commits: await this.getPRCommits(repo, pr.number)
|
|
3222
|
+
}))
|
|
3223
|
+
);
|
|
3224
|
+
|
|
3225
|
+
return this.formatChangelog(stack, entries);
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
private detectChangeType(pr: StackedPR): ChangeType {
|
|
3229
|
+
const title = pr.title.toLowerCase();
|
|
3230
|
+
const branch = pr.branch.toLowerCase();
|
|
3231
|
+
|
|
3232
|
+
// Detectar por prefijo convencional
|
|
3233
|
+
if (title.match(/^(feat|feature)/)) return 'feat';
|
|
3234
|
+
if (title.match(/^fix/)) return 'fix';
|
|
3235
|
+
if (title.match(/^(refactor|refactoring)/)) return 'refactor';
|
|
3236
|
+
if (title.match(/^(docs|documentation)/)) return 'docs';
|
|
3237
|
+
if (title.match(/^(test|tests)/)) return 'test';
|
|
3238
|
+
if (title.match(/^(chore|build|ci)/)) return 'chore';
|
|
3239
|
+
if (title.match(/^(perf|performance)/)) return 'perf';
|
|
3240
|
+
if (title.match(/^style/)) return 'style';
|
|
3241
|
+
|
|
3242
|
+
// Detectar por contenido
|
|
3243
|
+
if (branch.includes('fix') || branch.includes('bug')) return 'fix';
|
|
3244
|
+
if (branch.includes('refactor')) return 'refactor';
|
|
3245
|
+
if (branch.includes('test')) return 'test';
|
|
3246
|
+
|
|
3247
|
+
return 'feat'; // Default
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
private detectScope(pr: StackedPR): string {
|
|
3251
|
+
// Intentar extraer scope del título: "feat(auth): ..."
|
|
3252
|
+
const scopeMatch = pr.title.match(/^\w+\(([^)]+)\)/);
|
|
3253
|
+
if (scopeMatch) return scopeMatch[1];
|
|
3254
|
+
|
|
3255
|
+
// Detectar por branch
|
|
3256
|
+
const branchParts = pr.branch.split('/');
|
|
3257
|
+
if (branchParts.length >= 2) {
|
|
3258
|
+
return branchParts[1].split('-')[0]; // feature/auth-login -> auth
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
// Detectar por archivos cambiados
|
|
3262
|
+
if (pr.filesChanged) {
|
|
3263
|
+
if (pr.filesChanged.some(f => f.includes('api/'))) return 'api';
|
|
3264
|
+
if (pr.filesChanged.some(f => f.includes('component'))) return 'ui';
|
|
3265
|
+
if (pr.filesChanged.some(f => f.includes('model'))) return 'db';
|
|
3266
|
+
if (pr.filesChanged.some(f => f.includes('test'))) return 'test';
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
return 'core';
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
private formatChangelog(
|
|
3273
|
+
stack: PRStack,
|
|
3274
|
+
entries: ChangelogEntry[]
|
|
3275
|
+
): string {
|
|
3276
|
+
const version = this.calculateVersion(entries);
|
|
3277
|
+
const date = new Date().toISOString().split('T')[0];
|
|
3278
|
+
|
|
3279
|
+
// Agrupar por tipo
|
|
3280
|
+
const grouped = this.groupByType(entries);
|
|
3281
|
+
|
|
3282
|
+
let changelog = `# Changelog
|
|
3283
|
+
|
|
3284
|
+
## [${version}] - ${date}
|
|
3285
|
+
|
|
3286
|
+
### Stack: ${stack.feature}
|
|
3287
|
+
|
|
3288
|
+
`;
|
|
3289
|
+
|
|
3290
|
+
// Breaking changes primero
|
|
3291
|
+
const breaking = entries.filter(e => e.breaking);
|
|
3292
|
+
if (breaking.length > 0) {
|
|
3293
|
+
changelog += `### ⚠️ BREAKING CHANGES
|
|
3294
|
+
|
|
3295
|
+
${breaking.map(e => `- **${e.scope}**: ${e.description} (#${e.prNumber})`).join('\n')}
|
|
3296
|
+
|
|
3297
|
+
`;
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// Features
|
|
3301
|
+
if (grouped.feat?.length > 0) {
|
|
3302
|
+
changelog += `### ✨ Features
|
|
3303
|
+
|
|
3304
|
+
${grouped.feat.map(e => `- **${e.scope}**: ${e.description} (#${e.prNumber})`).join('\n')}
|
|
3305
|
+
|
|
3306
|
+
`;
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
// Bug fixes
|
|
3310
|
+
if (grouped.fix?.length > 0) {
|
|
3311
|
+
changelog += `### 🐛 Bug Fixes
|
|
3312
|
+
|
|
3313
|
+
${grouped.fix.map(e => `- **${e.scope}**: ${e.description} (#${e.prNumber})`).join('\n')}
|
|
3314
|
+
|
|
3315
|
+
`;
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
// Performance
|
|
3319
|
+
if (grouped.perf?.length > 0) {
|
|
3320
|
+
changelog += `### ⚡ Performance
|
|
3321
|
+
|
|
3322
|
+
${grouped.perf.map(e => `- **${e.scope}**: ${e.description} (#${e.prNumber})`).join('\n')}
|
|
3323
|
+
|
|
3324
|
+
`;
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
// Refactor
|
|
3328
|
+
if (grouped.refactor?.length > 0) {
|
|
3329
|
+
changelog += `### ♻️ Refactoring
|
|
3330
|
+
|
|
3331
|
+
${grouped.refactor.map(e => `- **${e.scope}**: ${e.description} (#${e.prNumber})`).join('\n')}
|
|
3332
|
+
|
|
3333
|
+
`;
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
// Aprendizaje completado (específico de este agente)
|
|
3337
|
+
changelog += `### 📚 Learning Completed
|
|
3338
|
+
|
|
3339
|
+
${stack.prStack
|
|
3340
|
+
.flatMap(pr => pr.learningObjectives || [])
|
|
3341
|
+
.filter((obj, i, arr) => arr.indexOf(obj) === i)
|
|
3342
|
+
.map(obj => `- ✅ ${obj}`)
|
|
3343
|
+
.join('\n')}
|
|
3344
|
+
|
|
3345
|
+
`;
|
|
3346
|
+
|
|
3347
|
+
// PRs incluidos
|
|
3348
|
+
changelog += `### 📋 PRs Included
|
|
3349
|
+
|
|
3350
|
+
| # | Title | Type |
|
|
3351
|
+
|---|-------|------|
|
|
3352
|
+
${stack.prStack.map(pr => `| #${pr.number} | ${this.cleanTitle(pr.title)} | ${this.detectChangeType(pr)} |`).join('\n')}
|
|
3353
|
+
|
|
3354
|
+
`;
|
|
3355
|
+
|
|
3356
|
+
return changelog;
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
```
|
|
3360
|
+
|
|
3361
|
+
### GitHub Action para Changelog Automático
|
|
3362
|
+
|
|
3363
|
+
```yaml
|
|
3364
|
+
# .github/workflows/changelog.yml
|
|
3365
|
+
name: Generate Changelog
|
|
3366
|
+
|
|
3367
|
+
on:
|
|
3368
|
+
push:
|
|
3369
|
+
branches: [main]
|
|
3370
|
+
paths-ignore:
|
|
3371
|
+
- 'CHANGELOG.md'
|
|
3372
|
+
|
|
3373
|
+
jobs:
|
|
3374
|
+
generate-changelog:
|
|
3375
|
+
runs-on: ubuntu-latest
|
|
3376
|
+
permissions:
|
|
3377
|
+
contents: write
|
|
3378
|
+
|
|
3379
|
+
steps:
|
|
3380
|
+
- uses: actions/checkout@v4
|
|
3381
|
+
with:
|
|
3382
|
+
fetch-depth: 0
|
|
3383
|
+
|
|
3384
|
+
- name: Generate Changelog
|
|
3385
|
+
id: changelog
|
|
3386
|
+
run: |
|
|
3387
|
+
# Obtener PRs mergeados desde el último tag
|
|
3388
|
+
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
3389
|
+
|
|
3390
|
+
if [ -n "$LAST_TAG" ]; then
|
|
3391
|
+
COMMITS_SINCE="$LAST_TAG..HEAD"
|
|
3392
|
+
else
|
|
3393
|
+
COMMITS_SINCE="HEAD~50..HEAD"
|
|
3394
|
+
fi
|
|
3395
|
+
|
|
3396
|
+
# Generar changelog
|
|
3397
|
+
echo "# Changelog" > CHANGELOG_NEW.md
|
|
3398
|
+
echo "" >> CHANGELOG_NEW.md
|
|
3399
|
+
echo "## [Unreleased] - $(date +%Y-%m-%d)" >> CHANGELOG_NEW.md
|
|
3400
|
+
echo "" >> CHANGELOG_NEW.md
|
|
3401
|
+
|
|
3402
|
+
# Features
|
|
3403
|
+
FEATURES=$(git log $COMMITS_SINCE --oneline --grep="^feat" --grep="^feature" --regexp-ignore-case | head -20)
|
|
3404
|
+
if [ -n "$FEATURES" ]; then
|
|
3405
|
+
echo "### ✨ Features" >> CHANGELOG_NEW.md
|
|
3406
|
+
echo "" >> CHANGELOG_NEW.md
|
|
3407
|
+
echo "$FEATURES" | while read line; do
|
|
3408
|
+
HASH=$(echo "$line" | cut -d' ' -f1)
|
|
3409
|
+
MSG=$(echo "$line" | cut -d' ' -f2-)
|
|
3410
|
+
PR=$(git log -1 --format=%B $HASH | grep -oP '#\d+' | head -1)
|
|
3411
|
+
echo "- $MSG $PR" >> CHANGELOG_NEW.md
|
|
3412
|
+
done
|
|
3413
|
+
echo "" >> CHANGELOG_NEW.md
|
|
3414
|
+
fi
|
|
3415
|
+
|
|
3416
|
+
# Fixes
|
|
3417
|
+
FIXES=$(git log $COMMITS_SINCE --oneline --grep="^fix" --regexp-ignore-case | head -20)
|
|
3418
|
+
if [ -n "$FIXES" ]; then
|
|
3419
|
+
echo "### 🐛 Bug Fixes" >> CHANGELOG_NEW.md
|
|
3420
|
+
echo "" >> CHANGELOG_NEW.md
|
|
3421
|
+
echo "$FIXES" | while read line; do
|
|
3422
|
+
HASH=$(echo "$line" | cut -d' ' -f1)
|
|
3423
|
+
MSG=$(echo "$line" | cut -d' ' -f2-)
|
|
3424
|
+
PR=$(git log -1 --format=%B $HASH | grep -oP '#\d+' | head -1)
|
|
3425
|
+
echo "- $MSG $PR" >> CHANGELOG_NEW.md
|
|
3426
|
+
done
|
|
3427
|
+
echo "" >> CHANGELOG_NEW.md
|
|
3428
|
+
fi
|
|
3429
|
+
|
|
3430
|
+
# Combinar con changelog existente
|
|
3431
|
+
if [ -f "CHANGELOG.md" ]; then
|
|
3432
|
+
tail -n +2 CHANGELOG.md >> CHANGELOG_NEW.md
|
|
3433
|
+
fi
|
|
3434
|
+
|
|
3435
|
+
mv CHANGELOG_NEW.md CHANGELOG.md
|
|
3436
|
+
|
|
3437
|
+
- name: Commit Changelog
|
|
3438
|
+
run: |
|
|
3439
|
+
git config user.name "github-actions[bot]"
|
|
3440
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
3441
|
+
|
|
3442
|
+
git add CHANGELOG.md
|
|
3443
|
+
git diff --staged --quiet || git commit -m "docs: update changelog [skip ci]"
|
|
3444
|
+
git push
|
|
3445
|
+
```
|
|
3446
|
+
|
|
3447
|
+
---
|
|
3448
|
+
|
|
3449
|
+
## 🔔 Notificaciones Slack/Discord
|
|
3450
|
+
|
|
3451
|
+
### Configuración de Notificaciones
|
|
3452
|
+
|
|
3453
|
+
```typescript
|
|
3454
|
+
class NotificationManager {
|
|
3455
|
+
/**
|
|
3456
|
+
* Gestiona notificaciones a Slack y Discord
|
|
3457
|
+
*/
|
|
3458
|
+
|
|
3459
|
+
async notifyStackProgress(
|
|
3460
|
+
stack: PRStack,
|
|
3461
|
+
event: StackEvent
|
|
3462
|
+
): Promise<void> {
|
|
3463
|
+
const message = this.buildMessage(stack, event);
|
|
3464
|
+
|
|
3465
|
+
// Enviar a todos los canales configurados
|
|
3466
|
+
await Promise.all([
|
|
3467
|
+
this.sendToSlack(message),
|
|
3468
|
+
this.sendToDiscord(message)
|
|
3469
|
+
]);
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
private buildMessage(stack: PRStack, event: StackEvent): NotificationMessage {
|
|
3473
|
+
const progressBar = this.buildProgressBar(stack);
|
|
3474
|
+
|
|
3475
|
+
switch (event.type) {
|
|
3476
|
+
case 'pr_merged':
|
|
3477
|
+
return {
|
|
3478
|
+
title: `✅ PR Mergeado - Stack: ${stack.feature}`,
|
|
3479
|
+
color: '#00C853',
|
|
3480
|
+
fields: [
|
|
3481
|
+
{ name: 'PR', value: `#${event.pr.number}`, inline: true },
|
|
3482
|
+
{ name: 'Progreso', value: progressBar, inline: true },
|
|
3483
|
+
{ name: 'Siguiente', value: event.nextPR ? `#${event.nextPR.number}` : 'Stack completado!' }
|
|
3484
|
+
]
|
|
3485
|
+
};
|
|
3486
|
+
|
|
3487
|
+
case 'pr_ready':
|
|
3488
|
+
return {
|
|
3489
|
+
title: `🔔 PR Listo para Review - Stack: ${stack.feature}`,
|
|
3490
|
+
color: '#2196F3',
|
|
3491
|
+
fields: [
|
|
3492
|
+
{ name: 'PR', value: `#${event.pr.number}`, inline: true },
|
|
3493
|
+
{ name: 'Título', value: event.pr.title },
|
|
3494
|
+
{ name: 'Estimado', value: event.pr.estimatedReviewTime || '30 min' }
|
|
3495
|
+
]
|
|
3496
|
+
};
|
|
3497
|
+
|
|
3498
|
+
case 'stack_completed':
|
|
3499
|
+
return {
|
|
3500
|
+
title: `🎉 Stack Completado: ${stack.feature}`,
|
|
3501
|
+
color: '#4CAF50',
|
|
3502
|
+
fields: [
|
|
3503
|
+
{ name: 'PRs Mergeados', value: `${stack.prStack.length}` },
|
|
3504
|
+
{ name: 'Conceptos Aprendidos', value: stack.learningPath?.length.toString() || 'N/A' }
|
|
3505
|
+
]
|
|
3506
|
+
};
|
|
3507
|
+
|
|
3508
|
+
default:
|
|
3509
|
+
return {
|
|
3510
|
+
title: `📚 Stack Update: ${stack.feature}`,
|
|
3511
|
+
color: '#9E9E9E',
|
|
3512
|
+
fields: []
|
|
3513
|
+
};
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
private buildProgressBar(stack: PRStack): string {
|
|
3518
|
+
const total = stack.prStack.length;
|
|
3519
|
+
const completed = stack.prStack.filter(pr => pr.merged).length;
|
|
3520
|
+
const filled = '█'.repeat(completed);
|
|
3521
|
+
const empty = '░'.repeat(total - completed);
|
|
3522
|
+
return `${filled}${empty} ${completed}/${total}`;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
```
|
|
3526
|
+
|
|
3527
|
+
### GitHub Action para Notificaciones
|
|
3528
|
+
|
|
3529
|
+
```yaml
|
|
3530
|
+
# .github/workflows/notify.yml
|
|
3531
|
+
name: Stack Notifications
|
|
3532
|
+
|
|
3533
|
+
on:
|
|
3534
|
+
pull_request:
|
|
3535
|
+
types: [closed, ready_for_review]
|
|
3536
|
+
|
|
3537
|
+
jobs:
|
|
3538
|
+
notify:
|
|
3539
|
+
runs-on: ubuntu-latest
|
|
3540
|
+
steps:
|
|
3541
|
+
- name: Detect Stack Info
|
|
3542
|
+
id: stack
|
|
3543
|
+
env:
|
|
3544
|
+
BRANCH: ${{ github.event.pull_request.head.ref }}
|
|
3545
|
+
run: |
|
|
3546
|
+
|
|
3547
|
+
if [[ $BRANCH =~ ^feature/([^/]+)/([0-9]+)- ]]; then
|
|
3548
|
+
echo "feature=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
|
|
3549
|
+
echo "position=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
|
|
3550
|
+
echo "is_stack=true" >> $GITHUB_OUTPUT
|
|
3551
|
+
|
|
3552
|
+
# Contar PRs del stack
|
|
3553
|
+
TOTAL=$(gh pr list --search "head:feature/${BASH_REMATCH[1]}/" --json number | jq length)
|
|
3554
|
+
MERGED=$(gh pr list --search "head:feature/${BASH_REMATCH[1]}/ is:merged" --json number | jq length)
|
|
3555
|
+
|
|
3556
|
+
echo "total=$TOTAL" >> $GITHUB_OUTPUT
|
|
3557
|
+
echo "merged=$MERGED" >> $GITHUB_OUTPUT
|
|
3558
|
+
else
|
|
3559
|
+
echo "is_stack=false" >> $GITHUB_OUTPUT
|
|
3560
|
+
fi
|
|
3561
|
+
env:
|
|
3562
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
3563
|
+
|
|
3564
|
+
# Notificación Slack
|
|
3565
|
+
- name: Notify Slack
|
|
3566
|
+
if: steps.stack.outputs.is_stack == 'true' && env.SLACK_WEBHOOK != ''
|
|
3567
|
+
env:
|
|
3568
|
+
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
|
3569
|
+
FEATURE: ${{ steps.stack.outputs.feature }}
|
|
3570
|
+
POSITION: ${{ steps.stack.outputs.position }}
|
|
3571
|
+
TOTAL: ${{ steps.stack.outputs.total }}
|
|
3572
|
+
MERGED: ${{ steps.stack.outputs.merged }}
|
|
3573
|
+
EVENT: ${{ github.event.action }}
|
|
3574
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
3575
|
+
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
3576
|
+
PR_MERGED: ${{ github.event.pull_request.merged }}
|
|
3577
|
+
run: |
|
|
3578
|
+
if [ "$EVENT" = "closed" ] && [ "$PR_MERGED" = "true" ]; then
|
|
3579
|
+
COLOR="#00C853"
|
|
3580
|
+
EMOJI="✅"
|
|
3581
|
+
STATUS="mergeado"
|
|
3582
|
+
else
|
|
3583
|
+
COLOR="#2196F3"
|
|
3584
|
+
EMOJI="🔔"
|
|
3585
|
+
STATUS="listo para review"
|
|
3586
|
+
fi
|
|
3587
|
+
|
|
3588
|
+
# Construir progress bar
|
|
3589
|
+
PROGRESS=""
|
|
3590
|
+
for i in $(seq 1 $TOTAL); do
|
|
3591
|
+
if [ $i -le $MERGED ]; then
|
|
3592
|
+
PROGRESS="${PROGRESS}█"
|
|
3593
|
+
else
|
|
3594
|
+
PROGRESS="${PROGRESS}░"
|
|
3595
|
+
fi
|
|
3596
|
+
done
|
|
3597
|
+
|
|
3598
|
+
curl -X POST "$SLACK_WEBHOOK" \
|
|
3599
|
+
-H 'Content-Type: application/json' \
|
|
3600
|
+
-d "{
|
|
3601
|
+
\"attachments\": [{
|
|
3602
|
+
\"color\": \"$COLOR\",
|
|
3603
|
+
\"blocks\": [
|
|
3604
|
+
{
|
|
3605
|
+
\"type\": \"header\",
|
|
3606
|
+
\"text\": {
|
|
3607
|
+
\"type\": \"plain_text\",
|
|
3608
|
+
\"text\": \"$EMOJI Stack: $FEATURE\",
|
|
3609
|
+
\"emoji\": true
|
|
3610
|
+
}
|
|
3611
|
+
},
|
|
3612
|
+
{
|
|
3613
|
+
\"type\": \"section\",
|
|
3614
|
+
\"fields\": [
|
|
3615
|
+
{
|
|
3616
|
+
\"type\": \"mrkdwn\",
|
|
3617
|
+
\"text\": \"*PR #$POSITION $STATUS*\"
|
|
3618
|
+
},
|
|
3619
|
+
{
|
|
3620
|
+
\"type\": \"mrkdwn\",
|
|
3621
|
+
\"text\": \"*Progreso:* \`$PROGRESS\` $MERGED/$TOTAL\"
|
|
3622
|
+
}
|
|
3623
|
+
]
|
|
3624
|
+
},
|
|
3625
|
+
{
|
|
3626
|
+
\"type\": \"section\",
|
|
3627
|
+
\"text\": {
|
|
3628
|
+
\"type\": \"mrkdwn\",
|
|
3629
|
+
\"text\": \"<$PR_URL|$PR_TITLE>\"
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
]
|
|
3633
|
+
}]
|
|
3634
|
+
}"
|
|
3635
|
+
|
|
3636
|
+
# Notificación Discord
|
|
3637
|
+
- name: Notify Discord
|
|
3638
|
+
if: steps.stack.outputs.is_stack == 'true' && env.DISCORD_WEBHOOK != ''
|
|
3639
|
+
env:
|
|
3640
|
+
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
3641
|
+
FEATURE: ${{ steps.stack.outputs.feature }}
|
|
3642
|
+
POSITION: ${{ steps.stack.outputs.position }}
|
|
3643
|
+
TOTAL: ${{ steps.stack.outputs.total }}
|
|
3644
|
+
MERGED: ${{ steps.stack.outputs.merged }}
|
|
3645
|
+
EVENT: ${{ github.event.action }}
|
|
3646
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
3647
|
+
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
3648
|
+
PR_MERGED: ${{ github.event.pull_request.merged }}
|
|
3649
|
+
run: |
|
|
3650
|
+
if [ "$EVENT" = "closed" ] && [ "$PR_MERGED" = "true" ]; then
|
|
3651
|
+
COLOR="65280" # Green
|
|
3652
|
+
EMOJI="✅"
|
|
3653
|
+
STATUS="mergeado"
|
|
3654
|
+
else
|
|
3655
|
+
COLOR="3447003" # Blue
|
|
3656
|
+
EMOJI="🔔"
|
|
3657
|
+
STATUS="listo para review"
|
|
3658
|
+
fi
|
|
3659
|
+
|
|
3660
|
+
curl -X POST "$DISCORD_WEBHOOK" \
|
|
3661
|
+
-H 'Content-Type: application/json' \
|
|
3662
|
+
-d "{
|
|
3663
|
+
\"embeds\": [{
|
|
3664
|
+
\"title\": \"$EMOJI Stack: $FEATURE\",
|
|
3665
|
+
\"color\": $COLOR,
|
|
3666
|
+
\"fields\": [
|
|
3667
|
+
{
|
|
3668
|
+
\"name\": \"PR\",
|
|
3669
|
+
\"value\": \"#$POSITION $STATUS\",
|
|
3670
|
+
\"inline\": true
|
|
3671
|
+
},
|
|
3672
|
+
{
|
|
3673
|
+
\"name\": \"Progreso\",
|
|
3674
|
+
\"value\": \"$MERGED/$TOTAL\",
|
|
3675
|
+
\"inline\": true
|
|
3676
|
+
},
|
|
3677
|
+
{
|
|
3678
|
+
\"name\": \"Título\",
|
|
3679
|
+
\"value\": \"[$PR_TITLE]($PR_URL)\"
|
|
3680
|
+
}
|
|
3681
|
+
]
|
|
3682
|
+
}]
|
|
3683
|
+
}"
|
|
3684
|
+
```
|
|
3685
|
+
|
|
3686
|
+
---
|
|
3687
|
+
|
|
3688
|
+
## 🔙 Rollback Strategy para Stacks
|
|
3689
|
+
|
|
3690
|
+
### Estrategias de Rollback
|
|
3691
|
+
|
|
3692
|
+
```typescript
|
|
3693
|
+
class StackRollbackManager {
|
|
3694
|
+
/**
|
|
3695
|
+
* Gestiona rollbacks de stacks parcialmente mergeados
|
|
3696
|
+
*/
|
|
3697
|
+
|
|
3698
|
+
async rollbackStack(
|
|
3699
|
+
repo: Repository,
|
|
3700
|
+
stack: PRStack,
|
|
3701
|
+
options: RollbackOptions
|
|
3702
|
+
): Promise<RollbackResult> {
|
|
3703
|
+
const strategy = this.selectStrategy(stack, options);
|
|
3704
|
+
|
|
3705
|
+
switch (strategy) {
|
|
3706
|
+
case 'revert':
|
|
3707
|
+
return this.revertStrategy(repo, stack, options);
|
|
3708
|
+
|
|
3709
|
+
case 'feature-flag':
|
|
3710
|
+
return this.featureFlagStrategy(repo, stack, options);
|
|
3711
|
+
|
|
3712
|
+
case 'branch-reset':
|
|
3713
|
+
return this.branchResetStrategy(repo, stack, options);
|
|
3714
|
+
|
|
3715
|
+
default:
|
|
3716
|
+
throw new Error(`Unknown rollback strategy: ${strategy}`);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
/**
|
|
3721
|
+
* Selecciona la mejor estrategia de rollback
|
|
3722
|
+
*/
|
|
3723
|
+
private selectStrategy(
|
|
3724
|
+
stack: PRStack,
|
|
3725
|
+
options: RollbackOptions
|
|
3726
|
+
): RollbackStrategy {
|
|
3727
|
+
const mergedCount = stack.prStack.filter(pr => pr.merged).length;
|
|
3728
|
+
|
|
3729
|
+
// Si solo un PR fue mergeado, revert es simple
|
|
3730
|
+
if (mergedCount === 1) {
|
|
3731
|
+
return 'revert';
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
// Si hay feature flags configurados, usarlos
|
|
3735
|
+
if (options.hasFeatureFlags) {
|
|
3736
|
+
return 'feature-flag';
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
// Si el stack tiene cambios de DB complejos, preferir feature flag
|
|
3740
|
+
const hasDBMigrations = stack.prStack.some(pr =>
|
|
3741
|
+
pr.filesChanged?.some(f => f.includes('migration'))
|
|
3742
|
+
);
|
|
3743
|
+
if (hasDBMigrations && options.hasFeatureFlags) {
|
|
3744
|
+
return 'feature-flag';
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
// Default: revert commits
|
|
3748
|
+
return 'revert';
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
/**
|
|
3752
|
+
* Estrategia 1: Revert commits
|
|
3753
|
+
* Crea PRs de revert para cada PR mergeado
|
|
3754
|
+
*/
|
|
3755
|
+
private async revertStrategy(
|
|
3756
|
+
repo: Repository,
|
|
3757
|
+
stack: PRStack,
|
|
3758
|
+
options: RollbackOptions
|
|
3759
|
+
): Promise<RollbackResult> {
|
|
3760
|
+
const mergedPRs = stack.prStack
|
|
3761
|
+
.filter(pr => pr.merged)
|
|
3762
|
+
.reverse(); // Revertir en orden inverso
|
|
3763
|
+
|
|
3764
|
+
const revertPRs: PullRequest[] = [];
|
|
3765
|
+
|
|
3766
|
+
for (const pr of mergedPRs) {
|
|
3767
|
+
// Obtener el merge commit
|
|
3768
|
+
const mergeCommit = await this.getMergeCommit(repo, pr);
|
|
3769
|
+
|
|
3770
|
+
// Crear branch de revert
|
|
3771
|
+
const revertBranch = `revert/${pr.branch.replace('feature/', '')}`;
|
|
3772
|
+
|
|
3773
|
+
await this.githubMCP.createBranch(repo, revertBranch, 'develop');
|
|
3774
|
+
|
|
3775
|
+
// Ejecutar git revert
|
|
3776
|
+
// Esto requiere un workflow o GitHub Action
|
|
3777
|
+
const revertPR = await this.createRevertPR(repo, pr, revertBranch);
|
|
3778
|
+
|
|
3779
|
+
revertPRs.push(revertPR);
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
return {
|
|
3783
|
+
strategy: 'revert',
|
|
3784
|
+
revertPRs,
|
|
3785
|
+
instructions: `
|
|
3786
|
+
## 🔙 Rollback del Stack: ${stack.feature}
|
|
3787
|
+
|
|
3788
|
+
### PRs de Revert Creados
|
|
3789
|
+
|
|
3790
|
+
${revertPRs.map(pr => `- [ ] #${pr.number} - Revert: ${pr.title}`).join('\n')}
|
|
3791
|
+
|
|
3792
|
+
### Instrucciones
|
|
3793
|
+
|
|
3794
|
+
1. Review y merge los PRs de revert **en orden**
|
|
3795
|
+
2. Verificar que la funcionalidad fue removida
|
|
3796
|
+
3. Si hay migraciones de DB, ejecutar rollback manualmente
|
|
3797
|
+
|
|
3798
|
+
### Comandos útiles
|
|
3799
|
+
|
|
3800
|
+
\`\`\`bash
|
|
3801
|
+
# Verificar estado después del rollback
|
|
3802
|
+
git log --oneline develop -10
|
|
3803
|
+
|
|
3804
|
+
# Si necesitas hacer rollback manual
|
|
3805
|
+
git revert <commit-hash>
|
|
3806
|
+
\`\`\`
|
|
3807
|
+
`
|
|
3808
|
+
};
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
/**
|
|
3812
|
+
* Estrategia 2: Feature Flags
|
|
3813
|
+
* Desactiva la funcionalidad sin remover código
|
|
3814
|
+
*/
|
|
3815
|
+
private async featureFlagStrategy(
|
|
3816
|
+
repo: Repository,
|
|
3817
|
+
stack: PRStack,
|
|
3818
|
+
options: RollbackOptions
|
|
3819
|
+
): Promise<RollbackResult> {
|
|
3820
|
+
const flagName = this.generateFlagName(stack.feature);
|
|
3821
|
+
|
|
3822
|
+
// Crear PR para desactivar feature flag
|
|
3823
|
+
const disablePR = await this.createFeatureFlagPR(repo, stack, flagName, false);
|
|
3824
|
+
|
|
3825
|
+
return {
|
|
3826
|
+
strategy: 'feature-flag',
|
|
3827
|
+
flagName,
|
|
3828
|
+
disablePR,
|
|
3829
|
+
instructions: `
|
|
3830
|
+
## 🚩 Rollback via Feature Flag: ${stack.feature}
|
|
3831
|
+
|
|
3832
|
+
### Feature Flag
|
|
3833
|
+
|
|
3834
|
+
\`\`\`
|
|
3835
|
+
Flag: ${flagName}
|
|
3836
|
+
Status: DISABLED
|
|
3837
|
+
\`\`\`
|
|
3838
|
+
|
|
3839
|
+
### PR Creado
|
|
3840
|
+
|
|
3841
|
+
- [ ] #${disablePR.number} - Disable feature flag: ${flagName}
|
|
3842
|
+
|
|
3843
|
+
### Para servicios de Feature Flags
|
|
3844
|
+
|
|
3845
|
+
#### LaunchDarkly
|
|
3846
|
+
\`\`\`bash
|
|
3847
|
+
ldcli flags update --flag=${flagName} --on=false
|
|
3848
|
+
\`\`\`
|
|
3849
|
+
|
|
3850
|
+
#### Unleash
|
|
3851
|
+
\`\`\`bash
|
|
3852
|
+
curl -X POST "https://unleash.example.com/api/admin/features/${flagName}/toggle/off"
|
|
3853
|
+
\`\`\`
|
|
3854
|
+
|
|
3855
|
+
#### ConfigCat
|
|
3856
|
+
\`\`\`bash
|
|
3857
|
+
configcat flag update ${flagName} --value false
|
|
3858
|
+
\`\`\`
|
|
3859
|
+
|
|
3860
|
+
#### Archivo de configuración
|
|
3861
|
+
\`\`\`json
|
|
3862
|
+
// config/features.json
|
|
3863
|
+
{
|
|
3864
|
+
"${flagName}": false
|
|
3865
|
+
}
|
|
3866
|
+
\`\`\`
|
|
3867
|
+
|
|
3868
|
+
### Ventajas de este approach
|
|
3869
|
+
|
|
3870
|
+
1. ✅ Rollback instantáneo (sin deploy)
|
|
3871
|
+
2. ✅ No requiere revert de código
|
|
3872
|
+
3. ✅ Fácil de re-activar cuando esté listo
|
|
3873
|
+
4. ✅ No afecta migraciones de DB
|
|
3874
|
+
`
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
/**
|
|
3879
|
+
* Estrategia 3: Branch Reset
|
|
3880
|
+
* Reset de develop a un commit anterior (último recurso)
|
|
3881
|
+
*/
|
|
3882
|
+
private async branchResetStrategy(
|
|
3883
|
+
repo: Repository,
|
|
3884
|
+
stack: PRStack,
|
|
3885
|
+
options: RollbackOptions
|
|
3886
|
+
): Promise<RollbackResult> {
|
|
3887
|
+
// Encontrar el commit antes del primer PR del stack
|
|
3888
|
+
const firstPR = stack.prStack.find(pr => pr.merged);
|
|
3889
|
+
const commitBeforeStack = await this.getCommitBefore(repo, firstPR);
|
|
3890
|
+
|
|
3891
|
+
return {
|
|
3892
|
+
strategy: 'branch-reset',
|
|
3893
|
+
targetCommit: commitBeforeStack,
|
|
3894
|
+
instructions: `
|
|
3895
|
+
## ⚠️ Rollback via Branch Reset: ${stack.feature}
|
|
3896
|
+
|
|
3897
|
+
### ⚠️ ADVERTENCIA
|
|
3898
|
+
|
|
3899
|
+
Este es un **rollback destructivo**. Úsalo solo como último recurso.
|
|
3900
|
+
|
|
3901
|
+
### Commit Target
|
|
3902
|
+
|
|
3903
|
+
\`\`\`
|
|
3904
|
+
${commitBeforeStack}
|
|
3905
|
+
\`\`\`
|
|
3906
|
+
|
|
3907
|
+
### Pasos
|
|
3908
|
+
|
|
3909
|
+
1. **Crear backup de develop actual**
|
|
3910
|
+
\`\`\`bash
|
|
3911
|
+
git checkout develop
|
|
3912
|
+
git checkout -b backup/develop-before-rollback
|
|
3913
|
+
git push origin backup/develop-before-rollback
|
|
3914
|
+
\`\`\`
|
|
3915
|
+
|
|
3916
|
+
2. **Deshabilitar branch protection temporalmente**
|
|
3917
|
+
- Ve a Settings → Branches → develop
|
|
3918
|
+
- Deshabilita protección temporalmente
|
|
3919
|
+
|
|
3920
|
+
3. **Ejecutar reset**
|
|
3921
|
+
\`\`\`bash
|
|
3922
|
+
git checkout develop
|
|
3923
|
+
git reset --hard ${commitBeforeStack}
|
|
3924
|
+
git push origin develop --force
|
|
3925
|
+
\`\`\`
|
|
3926
|
+
|
|
3927
|
+
4. **Re-habilitar branch protection**
|
|
3928
|
+
|
|
3929
|
+
5. **Notificar al equipo**
|
|
3930
|
+
- Todos deben hacer \`git fetch && git reset --hard origin/develop\`
|
|
3931
|
+
|
|
3932
|
+
### Riesgos
|
|
3933
|
+
|
|
3934
|
+
- ❌ Otros PRs mergeados después del stack también se perderán
|
|
3935
|
+
- ❌ Historial de git se reescribe
|
|
3936
|
+
- ❌ Puede causar conflictos en branches activos
|
|
3937
|
+
`
|
|
3938
|
+
};
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
private async createRevertPR(
|
|
3942
|
+
repo: Repository,
|
|
3943
|
+
originalPR: MergedPR,
|
|
3944
|
+
revertBranch: string
|
|
3945
|
+
): Promise<PullRequest> {
|
|
3946
|
+
return await this.githubMCP.createPullRequest(repo, {
|
|
3947
|
+
title: `🔙 Revert: ${originalPR.title}`,
|
|
3948
|
+
body: `
|
|
3949
|
+
## Revert PR #${originalPR.number}
|
|
3950
|
+
|
|
3951
|
+
Este PR revierte los cambios de #${originalPR.number}.
|
|
3952
|
+
|
|
3953
|
+
### Razón del Revert
|
|
3954
|
+
|
|
3955
|
+
<!-- Describir por qué se está haciendo rollback -->
|
|
3956
|
+
|
|
3957
|
+
### Checklist
|
|
3958
|
+
|
|
3959
|
+
- [ ] Verificar que el revert no rompe otras funcionalidades
|
|
3960
|
+
- [ ] Tests pasan
|
|
3961
|
+
- [ ] No hay errores en logs
|
|
3962
|
+
|
|
3963
|
+
### Original PR
|
|
3964
|
+
|
|
3965
|
+
- PR: #${originalPR.number}
|
|
3966
|
+
- Branch: \`${originalPR.branch}\`
|
|
3967
|
+
- Merged: ${originalPR.mergedAt}
|
|
3968
|
+
|
|
3969
|
+
---
|
|
3970
|
+
_Generado automáticamente por Stack Rollback Manager_
|
|
3971
|
+
`,
|
|
3972
|
+
head: revertBranch,
|
|
3973
|
+
base: 'develop'
|
|
3974
|
+
});
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
```
|
|
3978
|
+
|
|
3979
|
+
### GitHub Action para Rollback
|
|
3980
|
+
|
|
3981
|
+
```yaml
|
|
3982
|
+
# .github/workflows/stack-rollback.yml
|
|
3983
|
+
name: Stack Rollback
|
|
3984
|
+
|
|
3985
|
+
on:
|
|
3986
|
+
workflow_dispatch:
|
|
3987
|
+
inputs:
|
|
3988
|
+
feature:
|
|
3989
|
+
description: 'Feature name del stack (ej: user-auth)'
|
|
3990
|
+
required: true
|
|
3991
|
+
type: string
|
|
3992
|
+
strategy:
|
|
3993
|
+
description: 'Estrategia de rollback'
|
|
3994
|
+
required: true
|
|
3995
|
+
type: choice
|
|
3996
|
+
options:
|
|
3997
|
+
- revert
|
|
3998
|
+
- feature-flag
|
|
3999
|
+
rollback_to:
|
|
4000
|
+
description: 'Número de PR hasta donde hacer rollback (opcional)'
|
|
4001
|
+
required: false
|
|
4002
|
+
type: string
|
|
4003
|
+
|
|
4004
|
+
jobs:
|
|
4005
|
+
rollback:
|
|
4006
|
+
runs-on: ubuntu-latest
|
|
4007
|
+
permissions:
|
|
4008
|
+
contents: write
|
|
4009
|
+
pull-requests: write
|
|
4010
|
+
|
|
4011
|
+
steps:
|
|
4012
|
+
- uses: actions/checkout@v4
|
|
4013
|
+
with:
|
|
4014
|
+
fetch-depth: 0
|
|
4015
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
4016
|
+
|
|
4017
|
+
- name: Get Stack PRs
|
|
4018
|
+
id: stack
|
|
4019
|
+
env:
|
|
4020
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
4021
|
+
run: |
|
|
4022
|
+
FEATURE="${{ github.event.inputs.feature }}"
|
|
4023
|
+
|
|
4024
|
+
# Obtener PRs mergeados del stack
|
|
4025
|
+
MERGED_PRS=$(gh pr list \
|
|
4026
|
+
--search "head:feature/${FEATURE}/ is:merged" \
|
|
4027
|
+
--json number,title,headRefName,mergeCommit \
|
|
4028
|
+
--jq 'sort_by(.headRefName) | reverse')
|
|
4029
|
+
|
|
4030
|
+
echo "merged_prs=$MERGED_PRS" >> $GITHUB_OUTPUT
|
|
4031
|
+
echo "📚 PRs mergeados del stack:"
|
|
4032
|
+
echo "$MERGED_PRS" | jq -r '.[] | " #\(.number) - \(.title)"'
|
|
4033
|
+
|
|
4034
|
+
- name: Revert Strategy
|
|
4035
|
+
if: github.event.inputs.strategy == 'revert'
|
|
4036
|
+
env:
|
|
4037
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
4038
|
+
run: |
|
|
4039
|
+
FEATURE="${{ github.event.inputs.feature }}"
|
|
4040
|
+
MERGED_PRS='${{ steps.stack.outputs.merged_prs }}'
|
|
4041
|
+
|
|
4042
|
+
echo "$MERGED_PRS" | jq -c '.[]' | while read pr; do
|
|
4043
|
+
PR_NUM=$(echo "$pr" | jq -r '.number')
|
|
4044
|
+
PR_TITLE=$(echo "$pr" | jq -r '.title')
|
|
4045
|
+
MERGE_COMMIT=$(echo "$pr" | jq -r '.mergeCommit.oid')
|
|
4046
|
+
BRANCH_NAME=$(echo "$pr" | jq -r '.headRefName')
|
|
4047
|
+
|
|
4048
|
+
echo "🔙 Revirtiendo PR #$PR_NUM"
|
|
4049
|
+
|
|
4050
|
+
# Crear branch de revert
|
|
4051
|
+
REVERT_BRANCH="revert/${BRANCH_NAME#feature/}"
|
|
4052
|
+
git checkout develop
|
|
4053
|
+
git pull origin develop
|
|
4054
|
+
git checkout -b "$REVERT_BRANCH"
|
|
4055
|
+
|
|
4056
|
+
# Ejecutar revert
|
|
4057
|
+
git revert "$MERGE_COMMIT" --no-edit || {
|
|
4058
|
+
echo "⚠️ Conflicto en revert. Requiere resolución manual."
|
|
4059
|
+
exit 1
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
# Push y crear PR
|
|
4063
|
+
git push origin "$REVERT_BRANCH"
|
|
4064
|
+
|
|
4065
|
+
gh pr create \
|
|
4066
|
+
--title "🔙 Revert: $PR_TITLE" \
|
|
4067
|
+
--body "Reverts #$PR_NUM
|
|
4068
|
+
|
|
4069
|
+
## Stack Rollback
|
|
4070
|
+
|
|
4071
|
+
Este PR es parte del rollback del stack \`$FEATURE\`.
|
|
4072
|
+
|
|
4073
|
+
### Original
|
|
4074
|
+
- PR: #$PR_NUM
|
|
4075
|
+
- Commit: $MERGE_COMMIT
|
|
4076
|
+
|
|
4077
|
+
---
|
|
4078
|
+
_Auto-generated by Stack Rollback workflow_" \
|
|
4079
|
+
--base develop \
|
|
4080
|
+
--head "$REVERT_BRANCH"
|
|
4081
|
+
done
|
|
4082
|
+
|
|
4083
|
+
- name: Feature Flag Strategy
|
|
4084
|
+
if: github.event.inputs.strategy == 'feature-flag'
|
|
4085
|
+
run: |
|
|
4086
|
+
FEATURE="${{ github.event.inputs.feature }}"
|
|
4087
|
+
FLAG_NAME="feature_${FEATURE//-/_}"
|
|
4088
|
+
|
|
4089
|
+
echo "🚩 Desactivando feature flag: $FLAG_NAME"
|
|
4090
|
+
|
|
4091
|
+
# Crear branch para deshabilitar flag
|
|
4092
|
+
git checkout -b "disable-flag/${FEATURE}"
|
|
4093
|
+
|
|
4094
|
+
# Buscar y actualizar archivos de configuración
|
|
4095
|
+
# Esto depende de cómo se manejen los feature flags en el proyecto
|
|
4096
|
+
|
|
4097
|
+
# Ejemplo: archivo JSON
|
|
4098
|
+
if [ -f "config/features.json" ]; then
|
|
4099
|
+
jq ".[\"$FLAG_NAME\"] = false" config/features.json > tmp.json
|
|
4100
|
+
mv tmp.json config/features.json
|
|
4101
|
+
fi
|
|
4102
|
+
|
|
4103
|
+
# Ejemplo: archivo .env
|
|
4104
|
+
if [ -f ".env.example" ]; then
|
|
4105
|
+
echo "${FLAG_NAME^^}=false" >> .env.example
|
|
4106
|
+
fi
|
|
4107
|
+
|
|
4108
|
+
git add .
|
|
4109
|
+
git commit -m "chore: disable feature flag $FLAG_NAME" || echo "No changes"
|
|
4110
|
+
git push origin "disable-flag/${FEATURE}"
|
|
4111
|
+
|
|
4112
|
+
gh pr create \
|
|
4113
|
+
--title "🚩 Disable feature: $FEATURE" \
|
|
4114
|
+
--body "## Feature Flag Rollback
|
|
4115
|
+
|
|
4116
|
+
Desactiva el feature flag \`$FLAG_NAME\` para hacer rollback del stack \`$FEATURE\`.
|
|
4117
|
+
|
|
4118
|
+
### Acción requerida
|
|
4119
|
+
|
|
4120
|
+
Después de mergear este PR, también desactiva el flag en tu servicio de feature flags si usas uno externo.
|
|
4121
|
+
|
|
4122
|
+
---
|
|
4123
|
+
_Auto-generated by Stack Rollback workflow_" \
|
|
4124
|
+
--base develop \
|
|
4125
|
+
--head "disable-flag/${FEATURE}"
|
|
4126
|
+
|
|
4127
|
+
- name: Create Rollback Issue
|
|
4128
|
+
env:
|
|
4129
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
4130
|
+
run: |
|
|
4131
|
+
FEATURE="${{ github.event.inputs.feature }}"
|
|
4132
|
+
STRATEGY="${{ github.event.inputs.strategy }}"
|
|
4133
|
+
|
|
4134
|
+
gh issue create \
|
|
4135
|
+
--title "🔙 Rollback: Stack $FEATURE" \
|
|
4136
|
+
--body "## Stack Rollback en Progreso
|
|
4137
|
+
|
|
4138
|
+
### Detalles
|
|
4139
|
+
|
|
4140
|
+
- **Stack:** $FEATURE
|
|
4141
|
+
- **Estrategia:** $STRATEGY
|
|
4142
|
+
- **Iniciado por:** @${{ github.actor }}
|
|
4143
|
+
- **Fecha:** $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
4144
|
+
|
|
4145
|
+
### Status
|
|
4146
|
+
|
|
4147
|
+
- [ ] PRs de rollback creados
|
|
4148
|
+
- [ ] PRs de rollback mergeados
|
|
4149
|
+
- [ ] Verificación de rollback completado
|
|
4150
|
+
- [ ] Notificación al equipo
|
|
4151
|
+
|
|
4152
|
+
### PRs de Rollback
|
|
4153
|
+
|
|
4154
|
+
<!-- Los PRs de rollback se agregarán aquí -->
|
|
4155
|
+
|
|
4156
|
+
---
|
|
4157
|
+
_Auto-generated by Stack Rollback workflow_" \
|
|
4158
|
+
--label "rollback"
|
|
4159
|
+
```
|
|
4160
|
+
|
|
4161
|
+
---
|
|
4162
|
+
|
|
4163
|
+
## 📊 Detección Automática de Tech Stack
|
|
4164
|
+
|
|
4165
|
+
### Detector Multi-Lenguaje
|
|
4166
|
+
|
|
4167
|
+
```typescript
|
|
4168
|
+
class UniversalTechStackDetector {
|
|
4169
|
+
/**
|
|
4170
|
+
* Detecta el tech stack del proyecto de forma agnóstica
|
|
4171
|
+
*/
|
|
4172
|
+
async detect(projectPath: string): Promise<TechStack> {
|
|
4173
|
+
const files = await this.scanDirectory(projectPath);
|
|
4174
|
+
|
|
4175
|
+
return {
|
|
4176
|
+
language: this.detectLanguage(files),
|
|
4177
|
+
framework: this.detectFramework(files),
|
|
4178
|
+
packageManager: this.detectPackageManager(files),
|
|
4179
|
+
database: this.detectDatabase(files),
|
|
4180
|
+
runtime: this.detectRuntime(files),
|
|
4181
|
+
buildTool: this.detectBuildTool(files),
|
|
4182
|
+
testFramework: this.detectTestFramework(files),
|
|
4183
|
+
cicdPlatform: this.detectCICDPlatform(files),
|
|
4184
|
+
containerization: this.detectContainerization(files),
|
|
4185
|
+
cloudProvider: this.detectCloudProvider(files)
|
|
4186
|
+
};
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
private detectLanguage(files: string[]): LanguageInfo {
|
|
4190
|
+
const indicators: Record<string, LanguageConfig> = {
|
|
4191
|
+
// JavaScript / TypeScript
|
|
4192
|
+
'package.json': {
|
|
4193
|
+
language: 'javascript',
|
|
4194
|
+
checkForTypeScript: (f: string[]) => f.some(x => x.includes('tsconfig'))
|
|
4195
|
+
},
|
|
4196
|
+
|
|
4197
|
+
// Python
|
|
4198
|
+
'requirements.txt': { language: 'python' },
|
|
4199
|
+
'Pipfile': { language: 'python' },
|
|
4200
|
+
'pyproject.toml': { language: 'python' },
|
|
4201
|
+
'setup.py': { language: 'python' },
|
|
4202
|
+
|
|
4203
|
+
// Go
|
|
4204
|
+
'go.mod': { language: 'go' },
|
|
4205
|
+
|
|
4206
|
+
// Rust
|
|
4207
|
+
'Cargo.toml': { language: 'rust' },
|
|
4208
|
+
|
|
4209
|
+
// Java
|
|
4210
|
+
'pom.xml': { language: 'java', buildTool: 'maven' },
|
|
4211
|
+
'build.gradle': { language: 'java', buildTool: 'gradle' },
|
|
4212
|
+
'build.gradle.kts': { language: 'kotlin', buildTool: 'gradle' },
|
|
4213
|
+
|
|
4214
|
+
// Ruby
|
|
4215
|
+
'Gemfile': { language: 'ruby' },
|
|
4216
|
+
|
|
4217
|
+
// PHP
|
|
4218
|
+
'composer.json': { language: 'php' },
|
|
4219
|
+
|
|
4220
|
+
// C# / .NET
|
|
4221
|
+
'*.csproj': { language: 'csharp' },
|
|
4222
|
+
'*.sln': { language: 'csharp' },
|
|
4223
|
+
|
|
4224
|
+
// Elixir
|
|
4225
|
+
'mix.exs': { language: 'elixir' },
|
|
4226
|
+
|
|
4227
|
+
// Swift
|
|
4228
|
+
'Package.swift': { language: 'swift' },
|
|
4229
|
+
|
|
4230
|
+
// Dart / Flutter
|
|
4231
|
+
'pubspec.yaml': { language: 'dart' },
|
|
4232
|
+
|
|
4233
|
+
// Scala
|
|
4234
|
+
'build.sbt': { language: 'scala' },
|
|
4235
|
+
|
|
4236
|
+
// Clojure
|
|
4237
|
+
'project.clj': { language: 'clojure' },
|
|
4238
|
+
'deps.edn': { language: 'clojure' },
|
|
4239
|
+
};
|
|
4240
|
+
|
|
4241
|
+
for (const [indicator, config] of Object.entries(indicators)) {
|
|
4242
|
+
if (files.some(f => this.matchPattern(f, indicator))) {
|
|
4243
|
+
let language = config.language;
|
|
4244
|
+
|
|
4245
|
+
// Check for TypeScript
|
|
4246
|
+
if (config.checkForTypeScript && config.checkForTypeScript(files)) {
|
|
4247
|
+
language = 'typescript';
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
return {
|
|
4251
|
+
name: language,
|
|
4252
|
+
version: this.detectLanguageVersion(files, language),
|
|
4253
|
+
buildTool: config.buildTool
|
|
4254
|
+
};
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
return { name: 'unknown' };
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
private detectFramework(files: string[]): FrameworkInfo {
|
|
4262
|
+
const frameworkIndicators: Record<string, FrameworkConfig> = {
|
|
4263
|
+
// JavaScript Frameworks
|
|
4264
|
+
'next.config': { name: 'nextjs', type: 'fullstack', language: 'javascript' },
|
|
4265
|
+
'nuxt.config': { name: 'nuxt', type: 'fullstack', language: 'javascript' },
|
|
4266
|
+
'svelte.config': { name: 'sveltekit', type: 'fullstack', language: 'javascript' },
|
|
4267
|
+
'astro.config': { name: 'astro', type: 'frontend', language: 'javascript' },
|
|
4268
|
+
'remix.config': { name: 'remix', type: 'fullstack', language: 'javascript' },
|
|
4269
|
+
'angular.json': { name: 'angular', type: 'frontend', language: 'typescript' },
|
|
4270
|
+
'vite.config': { name: 'vite', type: 'frontend', language: 'javascript' },
|
|
4271
|
+
'gatsby-config': { name: 'gatsby', type: 'frontend', language: 'javascript' },
|
|
4272
|
+
'.eleventy': { name: 'eleventy', type: 'frontend', language: 'javascript' },
|
|
4273
|
+
'vue.config': { name: 'vue', type: 'frontend', language: 'javascript' },
|
|
4274
|
+
|
|
4275
|
+
// Node.js Backend
|
|
4276
|
+
'nest-cli.json': { name: 'nestjs', type: 'backend', language: 'typescript' },
|
|
4277
|
+
'express': { name: 'express', type: 'backend', language: 'javascript' },
|
|
4278
|
+
'fastify': { name: 'fastify', type: 'backend', language: 'javascript' },
|
|
4279
|
+
'koa': { name: 'koa', type: 'backend', language: 'javascript' },
|
|
4280
|
+
'hono': { name: 'hono', type: 'backend', language: 'javascript' },
|
|
4281
|
+
|
|
4282
|
+
// Python Frameworks
|
|
4283
|
+
'manage.py': { name: 'django', type: 'fullstack', language: 'python' },
|
|
4284
|
+
'fastapi': { name: 'fastapi', type: 'backend', language: 'python' },
|
|
4285
|
+
'flask': { name: 'flask', type: 'backend', language: 'python' },
|
|
4286
|
+
'starlette': { name: 'starlette', type: 'backend', language: 'python' },
|
|
4287
|
+
'tornado': { name: 'tornado', type: 'backend', language: 'python' },
|
|
4288
|
+
'pyramid': { name: 'pyramid', type: 'backend', language: 'python' },
|
|
4289
|
+
|
|
4290
|
+
// Go Frameworks
|
|
4291
|
+
'gin': { name: 'gin', type: 'backend', language: 'go' },
|
|
4292
|
+
'fiber': { name: 'fiber', type: 'backend', language: 'go' },
|
|
4293
|
+
'echo': { name: 'echo', type: 'backend', language: 'go' },
|
|
4294
|
+
'chi': { name: 'chi', type: 'backend', language: 'go' },
|
|
4295
|
+
|
|
4296
|
+
// Rust Frameworks
|
|
4297
|
+
'actix': { name: 'actix-web', type: 'backend', language: 'rust' },
|
|
4298
|
+
'axum': { name: 'axum', type: 'backend', language: 'rust' },
|
|
4299
|
+
'rocket': { name: 'rocket', type: 'backend', language: 'rust' },
|
|
4300
|
+
'warp': { name: 'warp', type: 'backend', language: 'rust' },
|
|
4301
|
+
|
|
4302
|
+
// Java/Kotlin Frameworks
|
|
4303
|
+
'spring': { name: 'spring-boot', type: 'backend', language: 'java' },
|
|
4304
|
+
'quarkus': { name: 'quarkus', type: 'backend', language: 'java' },
|
|
4305
|
+
'micronaut': { name: 'micronaut', type: 'backend', language: 'java' },
|
|
4306
|
+
'ktor': { name: 'ktor', type: 'backend', language: 'kotlin' },
|
|
4307
|
+
|
|
4308
|
+
// Ruby Frameworks
|
|
4309
|
+
'config/routes.rb': { name: 'rails', type: 'fullstack', language: 'ruby' },
|
|
4310
|
+
'sinatra': { name: 'sinatra', type: 'backend', language: 'ruby' },
|
|
4311
|
+
'hanami': { name: 'hanami', type: 'backend', language: 'ruby' },
|
|
4312
|
+
|
|
4313
|
+
// PHP Frameworks
|
|
4314
|
+
'artisan': { name: 'laravel', type: 'fullstack', language: 'php' },
|
|
4315
|
+
'symfony.lock': { name: 'symfony', type: 'backend', language: 'php' },
|
|
4316
|
+
'slim': { name: 'slim', type: 'backend', language: 'php' },
|
|
4317
|
+
|
|
4318
|
+
// Elixir Frameworks
|
|
4319
|
+
'phoenix': { name: 'phoenix', type: 'fullstack', language: 'elixir' },
|
|
4320
|
+
|
|
4321
|
+
// .NET Frameworks
|
|
4322
|
+
'Startup.cs': { name: 'aspnet-core', type: 'backend', language: 'csharp' },
|
|
4323
|
+
};
|
|
4324
|
+
|
|
4325
|
+
for (const [indicator, config] of Object.entries(frameworkIndicators)) {
|
|
4326
|
+
if (this.hasIndicator(files, indicator)) {
|
|
4327
|
+
return config;
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
return { name: 'generic', type: 'unknown' };
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
private detectDatabase(files: string[]): DatabaseInfo[] {
|
|
4335
|
+
const databases: DatabaseInfo[] = [];
|
|
4336
|
+
|
|
4337
|
+
const dbIndicators: Record<string, DatabaseInfo> = {
|
|
4338
|
+
// SQL Databases
|
|
4339
|
+
'prisma/schema.prisma': { type: 'sql', name: 'prisma' },
|
|
4340
|
+
'drizzle': { type: 'sql', name: 'drizzle' },
|
|
4341
|
+
'sequelize': { type: 'sql', name: 'sequelize' },
|
|
4342
|
+
'typeorm': { type: 'sql', name: 'typeorm' },
|
|
4343
|
+
'knexfile': { type: 'sql', name: 'knex' },
|
|
4344
|
+
'alembic': { type: 'sql', name: 'alembic' },
|
|
4345
|
+
'sqlalchemy': { type: 'sql', name: 'sqlalchemy' },
|
|
4346
|
+
'diesel': { type: 'sql', name: 'diesel' },
|
|
4347
|
+
'gorm': { type: 'sql', name: 'gorm' },
|
|
4348
|
+
'activerecord': { type: 'sql', name: 'activerecord' },
|
|
4349
|
+
'eloquent': { type: 'sql', name: 'eloquent' },
|
|
4350
|
+
'ecto': { type: 'sql', name: 'ecto' },
|
|
4351
|
+
|
|
4352
|
+
// NoSQL Databases
|
|
4353
|
+
'mongodb': { type: 'nosql', name: 'mongodb' },
|
|
4354
|
+
'mongoose': { type: 'nosql', name: 'mongoose' },
|
|
4355
|
+
'redis': { type: 'cache', name: 'redis' },
|
|
4356
|
+
'dynamodb': { type: 'nosql', name: 'dynamodb' },
|
|
4357
|
+
'firebase': { type: 'nosql', name: 'firebase' },
|
|
4358
|
+
'supabase': { type: 'sql', name: 'supabase' },
|
|
4359
|
+
};
|
|
4360
|
+
|
|
4361
|
+
for (const [indicator, dbInfo] of Object.entries(dbIndicators)) {
|
|
4362
|
+
if (this.hasIndicator(files, indicator)) {
|
|
4363
|
+
databases.push(dbInfo);
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4367
|
+
return databases;
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
private detectTestFramework(files: string[]): TestFrameworkInfo[] {
|
|
4371
|
+
const testFrameworks: TestFrameworkInfo[] = [];
|
|
4372
|
+
|
|
4373
|
+
const testIndicators: Record<string, TestFrameworkInfo> = {
|
|
4374
|
+
// JavaScript Testing
|
|
4375
|
+
'jest.config': { name: 'jest', language: 'javascript' },
|
|
4376
|
+
'vitest.config': { name: 'vitest', language: 'javascript' },
|
|
4377
|
+
'mocha': { name: 'mocha', language: 'javascript' },
|
|
4378
|
+
'playwright.config': { name: 'playwright', language: 'javascript', type: 'e2e' },
|
|
4379
|
+
'cypress.config': { name: 'cypress', language: 'javascript', type: 'e2e' },
|
|
4380
|
+
|
|
4381
|
+
// Python Testing
|
|
4382
|
+
'pytest.ini': { name: 'pytest', language: 'python' },
|
|
4383
|
+
'conftest.py': { name: 'pytest', language: 'python' },
|
|
4384
|
+
'unittest': { name: 'unittest', language: 'python' },
|
|
4385
|
+
|
|
4386
|
+
// Go Testing
|
|
4387
|
+
'_test.go': { name: 'go-test', language: 'go' },
|
|
4388
|
+
|
|
4389
|
+
// Rust Testing
|
|
4390
|
+
'#[test]': { name: 'rust-test', language: 'rust' },
|
|
4391
|
+
|
|
4392
|
+
// Java Testing
|
|
4393
|
+
'junit': { name: 'junit', language: 'java' },
|
|
4394
|
+
'testng': { name: 'testng', language: 'java' },
|
|
4395
|
+
|
|
4396
|
+
// Ruby Testing
|
|
4397
|
+
'rspec': { name: 'rspec', language: 'ruby' },
|
|
4398
|
+
'minitest': { name: 'minitest', language: 'ruby' },
|
|
4399
|
+
|
|
4400
|
+
// PHP Testing
|
|
4401
|
+
'phpunit.xml': { name: 'phpunit', language: 'php' },
|
|
4402
|
+
};
|
|
4403
|
+
|
|
4404
|
+
for (const [indicator, info] of Object.entries(testIndicators)) {
|
|
4405
|
+
if (this.hasIndicator(files, indicator)) {
|
|
4406
|
+
testFrameworks.push(info);
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4410
|
+
return testFrameworks;
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
private detectContainerization(files: string[]): ContainerInfo {
|
|
4414
|
+
if (files.some(f => f.includes('Dockerfile'))) {
|
|
4415
|
+
return {
|
|
4416
|
+
type: 'docker',
|
|
4417
|
+
hasCompose: files.some(f => f.includes('docker-compose')),
|
|
4418
|
+
hasKubernetes: files.some(f =>
|
|
4419
|
+
f.includes('k8s') ||
|
|
4420
|
+
f.includes('kubernetes') ||
|
|
4421
|
+
f.includes('helm')
|
|
4422
|
+
)
|
|
4423
|
+
};
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
if (files.some(f => f.includes('Containerfile'))) {
|
|
4427
|
+
return { type: 'podman' };
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
return { type: 'none' };
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
```
|
|
4434
|
+
|
|
4435
|
+
---
|
|
4436
|
+
|
|
4437
|
+
## 🎯 Actualización del Workflow Principal
|
|
4438
|
+
|
|
4439
|
+
```typescript
|
|
4440
|
+
class FreelancePlannerV4Enhanced {
|
|
4441
|
+
async executeFull(
|
|
4442
|
+
projectPath: string,
|
|
4443
|
+
options: PlannerOptions
|
|
4444
|
+
): Promise<ExecutionResult> {
|
|
4445
|
+
console.log('🚀 Freelance Project Planner v4.1 (Enhanced)');
|
|
4446
|
+
console.log('📚 GitFlow + Stacked PRs + Multi-Language Support\n');
|
|
4447
|
+
|
|
4448
|
+
// FASE 0: Detección de Tech Stack
|
|
4449
|
+
console.log('🔍 FASE 0: Detección de Tech Stack');
|
|
4450
|
+
const techStack = await this.techDetector.detect(projectPath);
|
|
4451
|
+
this.printTechStackSummary(techStack);
|
|
4452
|
+
|
|
4453
|
+
// FASE 1: Docker (adaptado al stack detectado)
|
|
4454
|
+
console.log('\n🐳 FASE 1: Dockerización');
|
|
4455
|
+
const dockerSetup = await this.dockerGenerator.generate(techStack);
|
|
4456
|
+
|
|
4457
|
+
// FASE 2: GitHub Actions (multi-lenguaje)
|
|
4458
|
+
console.log('\n⚙️ FASE 2: GitHub Actions');
|
|
4459
|
+
const workflows = await this.workflowGenerator.generate(techStack, {
|
|
4460
|
+
stackedPRs: true,
|
|
4461
|
+
previewEnvironments: options.previewEnvironments,
|
|
4462
|
+
notifications: options.notifications
|
|
4463
|
+
});
|
|
4464
|
+
|
|
4465
|
+
// FASE 3: GitFlow Setup
|
|
4466
|
+
console.log('\n🌳 FASE 3: GitFlow Setup');
|
|
4467
|
+
await this.setupGitFlow(options.repo);
|
|
4468
|
+
|
|
4469
|
+
// FASE 4: CODEOWNERS
|
|
4470
|
+
console.log('\n👥 FASE 4: CODEOWNERS');
|
|
4471
|
+
const codeowners = await this.codeownersGenerator.generate(projectPath);
|
|
4472
|
+
|
|
4473
|
+
// FASE 5: Stacked PR Tooling
|
|
4474
|
+
console.log('\n🛠️ FASE 5: Stacked PR Tooling');
|
|
4475
|
+
await this.setupStackTooling(projectPath);
|
|
4476
|
+
|
|
4477
|
+
// FASE 6: Planificación como Stacks
|
|
4478
|
+
console.log('\n📚 FASE 6: Generación de Stacks');
|
|
4479
|
+
const stacks = await this.planIterationsAsStacks(analysis);
|
|
4480
|
+
|
|
4481
|
+
// FASE 7: Notificaciones
|
|
4482
|
+
if (options.notifications) {
|
|
4483
|
+
console.log('\n🔔 FASE 7: Configuración de Notificaciones');
|
|
4484
|
+
await this.setupNotifications(options);
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
// FASE 8: Rollback Strategy
|
|
4488
|
+
console.log('\n🔙 FASE 8: Documentación de Rollback');
|
|
4489
|
+
const rollbackDocs = await this.generateRollbackDocs(stacks);
|
|
4490
|
+
|
|
4491
|
+
this.printFinalSummary({
|
|
4492
|
+
techStack,
|
|
4493
|
+
dockerSetup,
|
|
4494
|
+
workflows,
|
|
4495
|
+
codeowners,
|
|
4496
|
+
stacks,
|
|
4497
|
+
rollbackDocs
|
|
4498
|
+
});
|
|
4499
|
+
|
|
4500
|
+
return { /* ... */ };
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
```
|