aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
721 lines (559 loc) • 19.5 kB
Markdown
# github-pr-automation.md
**Task**: GitHub Pull Request Automation (Repository-Agnostic)
**Purpose**: Automate PR creation from story context using GitHub CLI, works with ANY repository.
**When to use**: After pushing feature branch, via `@github-devops *create-pr` command.
## Execution Modes
**Choose your execution mode:**
### 1. YOLO Mode - Fast, Autonomous (0-1 prompts)
- Autonomous decision making with logging
- Minimal user interaction
- **Best for:** Simple, deterministic tasks
### 2. Interactive Mode - Balanced, Educational (5-10 prompts) **[DEFAULT]**
- Explicit decision checkpoints
- Educational explanations
- **Best for:** Learning, complex decisions
### 3. Pre-Flight Planning - Comprehensive Upfront Planning
- Task analysis phase (identify all ambiguities)
- Zero ambiguity execution
- **Best for:** Ambiguous requirements, critical work
**Parameter:** `mode` (optional, default: `interactive`)
## Task Definition (AIOS Task Format V1.0)
```yaml
task: githubDevopsGithubPrAutomation()
responsável: Gage (Automator)
responsavel_type: Agente
atomic_layer: Organism
**Entrada:**
- campo: task
tipo: string
origem: User Input
obrigatório: true
validação: Must be registered task
- campo: parameters
tipo: object
origem: User Input
obrigatório: false
validação: Valid task parameters
- campo: mode
tipo: string
origem: User Input
obrigatório: false
validação: yolo|interactive|pre-flight
**Saída:**
- campo: execution_result
tipo: object
destino: Memory
persistido: false
- campo: logs
tipo: array
destino: File (.ai/logs/*)
persistido: true
- campo: state
tipo: object
destino: State management
persistido: true
```
## Pre-Conditions
**Purpose:** Validate prerequisites BEFORE task execution (blocking)
**Checklist:**
```yaml
pre-conditions:
- [ ] Task is registered; required parameters provided; dependencies met
tipo: pre-condition
blocker: true
validação: |
Check task is registered; required parameters provided; dependencies met
error_message: "Pre-condition failed: Task is registered; required parameters provided; dependencies met"
```
## Post-Conditions
**Purpose:** Validate execution success AFTER task completes
**Checklist:**
```yaml
post-conditions:
- [ ] Task completed; exit code 0; expected outputs created
tipo: post-condition
blocker: true
validação: |
Verify task completed; exit code 0; expected outputs created
error_message: "Post-condition failed: Task completed; exit code 0; expected outputs created"
```
## Acceptance Criteria
**Purpose:** Definitive pass/fail criteria for task completion
**Checklist:**
```yaml
acceptance-criteria:
- [ ] Task completed as expected; side effects documented
tipo: acceptance-criterion
blocker: true
validação: |
Assert task completed as expected; side effects documented
error_message: "Acceptance criterion not met: Task completed as expected; side effects documented"
```
## Tools
**External/shared resources used by this task:**
- **Tool:** task-runner
- **Purpose:** Task execution and orchestration
- **Source:** .aios-core/core/task-runner.js
- **Tool:** logger
- **Purpose:** Execution logging and error tracking
- **Source:** .aios-core/utils/logger.js
## Scripts
**Agent-specific code for this task:**
- **Script:** execute-task.js
- **Purpose:** Generic task execution wrapper
- **Language:** JavaScript
- **Location:** .aios-core/scripts/execute-task.js
## Error Handling
**Strategy:** retry
**Common Errors:**
1. **Error:** Task Not Found
- **Cause:** Specified task not registered in system
- **Resolution:** Verify task name and registration
- **Recovery:** List available tasks, suggest similar
2. **Error:** Invalid Parameters
- **Cause:** Task parameters do not match expected schema
- **Resolution:** Validate parameters against task definition
- **Recovery:** Provide parameter template, reject execution
3. **Error:** Execution Timeout
- **Cause:** Task exceeds maximum execution time
- **Resolution:** Optimize task or increase timeout
- **Recovery:** Kill task, cleanup resources, log state
## Performance
**Expected Metrics:**
```yaml
duration_expected: 5-15 min (estimated)
cost_estimated: $0.003-0.010
token_usage: ~3,000-10,000 tokens
```
**Optimization Notes:**
- Break into smaller workflows; implement checkpointing; use async processing where possible
## Metadata
```yaml
story: N/A
version: 1.0.0
dependencies:
- N/A
tags:
- automation
- workflow
updated_at: 2025-11-17
```
## Prerequisites
- GitHub CLI (`gh`) installed and authenticated
- Feature branch pushed to remote
- Repository context detected
- Story file (optional but recommended)
## Workflow Steps
### Step 1: Detect Repository Context
```javascript
const { detectRepositoryContext } = require('./../scripts/repository-detector');
const context = detectRepositoryContext();
if (!context) {
throw new Error('Unable to detect repository. Run "aios init" first.');
}
```
### Step 2: Get Current Branch
```bash
git branch --show-current
```
### Step 3: Extract Story Information (if available)
```javascript
function extractStoryInfo(storyPath) {
if (!storyPath || !fs.existsSync(storyPath)) {
return null;
}
const content = fs.readFileSync(storyPath, 'utf8');
// Extract story ID from path or content
const storyIdMatch = storyPath.match(/(\d+\.\d+)/);
const storyId = storyIdMatch ? storyIdMatch[1] : null;
// Extract title
const titleMatch = content.match(/title:\s*["']?([^"'\n]+)["']?/);
const title = titleMatch ? titleMatch[1] : null;
// Extract acceptance criteria
const acMatch = content.match(/acceptance_criteria:([\s\S]*?)(?=\n\w+:|$)/);
return {
id: storyId,
title,
hasAcceptanceCriteria: !!acMatch
};
}
```
### Step 4: Generate PR Title (Configurable Format)
> **Configuration-Driven:** PR title format is controlled by `core-config.yaml` → `github.pr.title_format`
> This allows each project to choose the format that matches their workflow.
```javascript
const yaml = require('js-yaml');
const fs = require('fs');
const path = require('path');
/**
* Load PR configuration from core-config.yaml
* @returns {Object} PR configuration with defaults
*/
function loadPRConfig() {
const configPath = path.join(process.cwd(), '.aios-core', 'core-config.yaml');
// Default configuration (for projects without core-config)
const defaults = {
title_format: 'story-first', // Safe default for most projects
include_story_id: true,
conventional_commits: {
enabled: false,
branch_type_map: {
'feature/': 'feat',
'feat/': 'feat',
'fix/': 'fix',
'bugfix/': 'fix',
'hotfix/': 'fix',
'docs/': 'docs',
'chore/': 'chore',
'refactor/': 'refactor',
'test/': 'test',
'perf/': 'perf',
'ci/': 'ci',
'style/': 'style',
'build/': 'build'
},
default_type: 'feat'
}
};
try {
if (fs.existsSync(configPath)) {
const config = yaml.load(fs.readFileSync(configPath, 'utf8'));
return { ...defaults, ...config?.github?.pr };
}
} catch (error) {
console.warn('Could not load core-config.yaml, using defaults');
}
return defaults;
}
/**
* Generate PR title based on project configuration.
*
* Supported formats (configured in core-config.yaml → github.pr.title_format):
*
* 1. "conventional" - Conventional Commits format (for semantic-release)
* Example: "feat(auth): implement OAuth login [Story 6.17]"
*
* 2. "story-first" - Story ID first (legacy/simple projects)
* Example: "[Story 6.17] Implement OAuth Login"
*
* 3. "branch-based" - Branch name converted to title
* Example: "Feature User Auth"
*
* @param {string} branchName - Current git branch name
* @param {Object} storyInfo - Story information (id, title)
* @returns {string} Formatted PR title
*/
function generatePRTitle(branchName, storyInfo) {
const config = loadPRConfig();
const format = config.title_format || 'story-first';
switch (format) {
case 'conventional':
return generateConventionalTitle(branchName, storyInfo, config);
case 'story-first':
return generateStoryFirstTitle(branchName, storyInfo, config);
case 'branch-based':
return generateBranchBasedTitle(branchName, storyInfo, config);
default:
return generateStoryFirstTitle(branchName, storyInfo, config);
}
}
/**
* Format: {type}({scope}): {description} [Story {id}]
* Used for: Projects with semantic-release automation
*/
function generateConventionalTitle(branchName, storyInfo, config) {
const typeMap = config.conventional_commits?.branch_type_map || {};
const defaultType = config.conventional_commits?.default_type || 'feat';
// Detect commit type from branch prefix
let type = defaultType;
for (const [prefix, commitType] of Object.entries(typeMap)) {
if (branchName.startsWith(prefix)) {
type = commitType;
break;
}
}
// Extract scope from branch name (e.g., feat/auth/login -> scope=auth)
const scopeMatch = branchName.match(/^[a-z-]+\/([a-z-]+)\//);
const scope = scopeMatch ? scopeMatch[1] : null;
const scopeStr = scope ? `(${scope})` : '';
// Generate description
if (storyInfo && storyInfo.id && storyInfo.title) {
let cleanTitle = storyInfo.title
.replace(/^Story\s*\d+\.\d+[:\s-]*/i, '')
.trim();
cleanTitle = cleanTitle.charAt(0).toLowerCase() + cleanTitle.slice(1);
const storyRef = config.include_story_id ? ` [Story ${storyInfo.id}]` : '';
return `${type}${scopeStr}: ${cleanTitle}${storyRef}`;
}
// Fallback: convert branch name to description
const description = branchName
.replace(/^(feature|feat|fix|bugfix|hotfix|docs|chore|refactor|test|perf|ci|style|build)\//, '')
.replace(/^[a-z-]+\//, '')
.replace(/-/g, ' ')
.toLowerCase()
.trim();
return `${type}${scopeStr}: ${description}`;
}
/**
* Format: [Story {id}] {Title}
* Used for: Simple projects, legacy workflows, non-NPM projects
*/
function generateStoryFirstTitle(branchName, storyInfo, config) {
if (storyInfo && storyInfo.id && storyInfo.title) {
return `[Story ${storyInfo.id}] ${storyInfo.title}`;
}
// Fallback: convert branch name to title
return branchName
.replace(/^(feature|feat|fix|bugfix|hotfix|docs|chore|refactor|test|perf|ci|style|build)\//, '')
.replace(/-/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
}
/**
* Format: {Branch Name As Title}
* Used for: Minimal projects, quick iterations
*/
function generateBranchBasedTitle(branchName, storyInfo, config) {
const title = branchName
.replace(/^(feature|feat|fix|bugfix|hotfix|docs|chore|refactor|test|perf|ci|style|build)\//, '')
.replace(/-/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
if (config.include_story_id && storyInfo?.id) {
return `${title} [Story ${storyInfo.id}]`;
}
return title;
}
```
## Configuration Reference
Add to your project's `core-config.yaml`:
```yaml
github:
pr:
# Options: conventional | story-first | branch-based
title_format: conventional # For semantic-release projects
# title_format: story-first # For simple projects (default)
include_story_id: true
conventional_commits:
enabled: true
branch_type_map:
feature/: feat
fix/: fix
docs/: docs
# Add custom mappings as needed
default_type: feat
semantic_release:
enabled: true # Set false if not using semantic-release
```
## Title Format Examples
| Format | Branch | Story | Generated Title |
|--------|--------|-------|-----------------|
| `conventional` | `feature/user-auth` | 6.17: User Auth | `feat: user auth [Story 6.17]` |
| `conventional` | `fix/cli/parsing` | 6.18: CLI Fix | `fix(cli): cLI fix [Story 6.18]` |
| `story-first` | `feature/user-auth` | 6.17: User Auth | `[Story 6.17] User Auth` |
| `story-first` | `fix/cli-bug` | - | `Cli Bug` |
| `branch-based` | `feature/user-auth` | 6.17 | `User Auth [Story 6.17]` |
| `branch-based` | `docs/readme` | - | `Readme` |
### Step 5: Generate PR Description
```javascript
function generatePRDescription(storyInfo, context) {
let description = `## Summary\n\n`;
if (storyInfo) {
description += `This PR implements Story ${storyInfo.id}: ${storyInfo.title}\n\n`;
description += `**Story File**: \`docs/stories/${storyInfo.id}-*.yaml\`\n\n`;
} else {
description += `Changes from branch: ${branchName}\n\n`;
}
description += `## Changes\n\n`;
description += `- [List main changes here]\n\n`;
description += `## Testing\n\n`;
description += `- [ ] Unit tests passing\n`;
description += `- [ ] Integration tests passing\n`;
description += `- [ ] Manual testing completed\n\n`;
description += `## Checklist\n\n`;
description += `- [ ] Code follows project standards\n`;
description += `- [ ] Tests added/updated\n`;
description += `- [ ] Documentation updated\n`;
description += `- [ ] Quality gates passed\n\n`;
description += `---\n`;
description += `**Repository**: ${context.repositoryUrl}\n`;
description += `**Mode**: ${context.mode}\n`;
description += `**Package**: ${context.packageName} v${context.packageVersion}\n`;
return description;
}
```
### Step 5.1: Enrich PR Description with Impact Analysis (Code Intelligence — Advisory)
> **Added by:** Story NOG-7 (DevOps Pre-Push Impact Analysis)
> **Behavior:** Auto-skips if code intelligence unavailable. Appends "Impact Analysis" section to PR body.
```javascript
const { generateImpactSummary } = require('.aios-core/core/code-intel/helpers/devops-helper');
async function enrichPRWithImpactAnalysis(description, changedFiles) {
// Auto-skip if code intelligence unavailable
const { isCodeIntelAvailable } = require('.aios-core/core/code-intel');
if (!isCodeIntelAvailable()) {
return description; // Return original description unchanged
}
const impact = await generateImpactSummary(changedFiles);
if (!impact) {
return description; // No impact data — return original
}
// Append Impact Analysis section to PR description
const impactSection = [
'',
'## Impact Analysis',
'',
impact.summary,
'',
'---',
'*Generated by Code Intelligence (advisory only)*',
].join('\n');
return description + impactSection;
}
```
**Usage in PR creation flow:**
After `generatePRDescription()` returns the base description, call `enrichPRWithImpactAnalysis()` to optionally append the impact section:
```javascript
let description = generatePRDescription(storyInfo, context);
description = await enrichPRWithImpactAnalysis(description, changedFiles);
```
**Important:** If code intelligence is unavailable or returns null, the PR description remains unchanged — zero impact on existing workflow.
### Step 6: Determine Base Branch
```javascript
function determineBaseBranch(projectRoot) {
// Check default branch from git
try {
const defaultBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
cwd: projectRoot
}).toString().trim().replace('refs/remotes/origin/', '');
return defaultBranch || 'main';
} catch (error) {
// Fallback to main
return 'main';
}
}
```
### Step 7: Create PR via GitHub CLI
```bash
gh pr create \
--title "{title}" \
--body "{description}" \
--base {baseBranch} \
--head {currentBranch}
```
### Step 8: Assign Reviewers (Optional)
```javascript
function assignReviewers(storyType, prNumber) {
const reviewerMap = {
'feature': ['@dev-team'],
'bugfix': ['@qa-team'],
'docs': ['@tech-writer'],
'security': ['@security-team']
};
const reviewers = reviewerMap[storyType] || ['@dev-team'];
execSync(`gh pr edit ${prNumber} --add-reviewer ${reviewers.join(',')}`, {
cwd: projectRoot
});
}
```
## Example Usage
```javascript
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
async function createPullRequest(storyPath) {
// Detect repository
const { detectRepositoryContext } = require('./../scripts/repository-detector');
const context = detectRepositoryContext();
console.log(`\n🔀 Creating Pull Request`);
console.log(`Repository: ${context.repositoryUrl}\n`);
// Get current branch
const currentBranch = execSync('git branch --show-current', {
cwd: context.projectRoot
}).toString().trim();
console.log(`Branch: ${currentBranch}`);
// Extract story info
const storyInfo = storyPath ? extractStoryInfo(storyPath) : null;
// Generate PR title and description
const title = generatePRTitle(currentBranch, storyInfo);
const description = generatePRDescription(storyInfo, context);
const baseBranch = determineBaseBranch(context.projectRoot);
console.log(`Title: ${title}`);
console.log(`Base: ${baseBranch}\n`);
// Create PR
const prUrl = execSync(
`gh pr create --title "${title}" --body "${description}" --base ${baseBranch}`,
{ cwd: context.projectRoot }
).toString().trim();
console.log(`\n✅ Pull Request created: ${prUrl}`);
return { prUrl, title, baseBranch };
}
module.exports = { createPullRequest };
```
## Integration
Called by `@github-devops` via `*create-pr` command.
## Validation
- PR created in correct repository (detected URL)
- PR title follows Conventional Commits format (required for semantic-release)
- PR title includes story ID if available (e.g., `[Story 6.17]`)
- PR description includes repository context
- Base branch is correct (usually main/master)
## Semantic-Release Integration (Optional)
> **Note:** This section only applies when `core-config.yaml` has:
> - `github.pr.title_format: conventional`
> - `github.semantic_release.enabled: true`
>
> Projects without semantic-release should use `title_format: story-first` (default).
**When enabled:** PRs merged via "Squash and merge" use the PR title as commit message, triggering semantic-release:
| Branch Pattern | Generated Title | Release |
|---------------|-----------------|---------|
| `feature/user-auth` | `feat: user auth` | ✅ Minor |
| `feat/auth/sso-login` | `feat(auth): sso login` | ✅ Minor |
| `fix/cli-parsing` | `fix: cli parsing` | ✅ Patch |
| `docs/readme-update` | `docs: readme update` | ❌ None |
| `chore/deps-update` | `chore: deps update` | ❌ None |
For breaking changes, manually edit the PR title to include `!`:
- `feat!: redesign authentication API [Story 7.1]`
## Configuration for Different Project Types
### NPM Package with Semantic-Release (aios-core)
```yaml
github:
pr:
title_format: conventional
semantic_release:
enabled: true
```
### Simple Web App (no releases)
```yaml
github:
pr:
title_format: story-first # [Story 6.17] Title
semantic_release:
enabled: false
```
### Quick Prototypes
```yaml
github:
pr:
title_format: branch-based # Just branch name as title
include_story_id: false
```
## Notes
- Works with ANY repository
- Gracefully handles missing story file
- Uses GitHub CLI for reliability
- Repository context from detector
## Handoff
next_agent: @po
next_command: *close-story {story-id}
condition: PR merged successfully
alternatives:
- agent: @dev, command: *apply-qa-fixes, condition: PR review requested changes