├── .github └── workflows │ ├── test-action.yml │ ├── pr-review.yml │ ├── describe-pr.yml │ ├── format-check.yml │ ├── on-demand-review.yml │ ├── on-demand-description.yml │ └── release.yml ├── .prettierrc.json ├── tsconfig.json ├── src ├── types │ ├── context.ts │ ├── github.ts │ └── inputs.ts ├── template │ ├── extractors │ │ ├── custom-context-extractor.ts │ │ ├── base-extractor.ts │ │ └── pr-extractor.ts │ ├── context-builder.ts │ ├── template-processor.ts │ └── template-engine.ts ├── utils │ ├── logger.ts │ ├── file-utils.ts │ └── validation.ts ├── config │ └── constants.ts ├── services │ └── github-service.ts └── index.ts ├── .prettierignore ├── LICENSE ├── package.json ├── example-workflows ├── pr-description.yml ├── code-review.yml ├── triage-issue.yml ├── README.md └── template-pr-review.yml ├── .gitignore ├── RELEASE.md ├── action.yml ├── README.md └── TEMPLATE.md /.github/workflows/test-action.yml: -------------------------------------------------------------------------------- 1 | name: Test Augment Agent Action 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | 7 | jobs: 8 | test-action: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | 14 | - name: Test Augment Agent 15 | uses: ./ 16 | with: 17 | augment_session_auth: ${{ secrets.AUGMENT_SESSION_AUTH }} 18 | instruction: "Say hello" 19 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid", 10 | "endOfLine": "lf", 11 | "overrides": [ 12 | { 13 | "files": "*.md", 14 | "options": { 15 | "printWidth": 80, 16 | "proseWrap": "preserve" 17 | } 18 | }, 19 | { 20 | "files": "*.yml", 21 | "options": { 22 | "tabWidth": 2, 23 | "singleQuote": false 24 | } 25 | }, 26 | { 27 | "files": "*.json", 28 | "options": { 29 | "tabWidth": 2 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/pr-review.yml: -------------------------------------------------------------------------------- 1 | # Basic PR Review Generation 2 | # Automatically generates AI-powered reviews for new pull requests 3 | 4 | name: Auto-review PRs 5 | on: 6 | pull_request: 7 | types: [opened] 8 | 9 | jobs: 10 | review: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | pull-requests: write 15 | steps: 16 | - name: Generate PR Review 17 | uses: augmentcode/review-pr@v0 18 | with: 19 | augment_session_auth: ${{ secrets.AUGMENT_SESSION_AUTH }} 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | pull_number: ${{ github.event.pull_request.number }} 22 | repo_name: ${{ github.repository }} 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "lib": ["ES2022"], 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "declaration": true, 12 | "declarationMap": true, 13 | "sourceMap": true, 14 | "resolveJsonModule": true, 15 | "allowSyntheticDefaultImports": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noUncheckedIndexedAccess": true, 19 | "exactOptionalPropertyTypes": true 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/describe-pr.yml: -------------------------------------------------------------------------------- 1 | # Basic PR Description Generation 2 | # Automatically generates AI-powered descriptions for new pull requests 3 | 4 | name: Auto-describe PRs 5 | on: 6 | pull_request: 7 | types: [opened] 8 | 9 | jobs: 10 | describe: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | pull-requests: write 15 | steps: 16 | - name: Generate PR Description 17 | uses: augmentcode/describe-pr@v0 18 | with: 19 | augment_session_auth: ${{ secrets.AUGMENT_SESSION_AUTH }} 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | pull_number: ${{ github.event.pull_request.number }} 22 | repo_name: ${{ github.repository }} 23 | -------------------------------------------------------------------------------- /src/types/context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Context-related type definitions 3 | */ 4 | 5 | import { PullRequestFile } from './github'; 6 | 7 | export interface GithubRepo { 8 | full_name: string; 9 | name: string; 10 | owner: string; 11 | } 12 | 13 | export interface GitRef { 14 | ref: string; 15 | sha: string; 16 | repo: GithubRepo; 17 | } 18 | 19 | export interface PRData { 20 | number: number; 21 | title: string; 22 | author: string; 23 | head: GitRef; 24 | base: GitRef; 25 | body: string; 26 | state: string; 27 | changed_files: string; // Newline-separated list of filenames 28 | changed_files_list: Array; 29 | diff_file: string; 30 | } 31 | 32 | export interface TemplateContext { 33 | // PR-related context (if PR info provided) 34 | pr?: PRData; 35 | 36 | // Custom context (parsed from JSON) 37 | custom?: Record; 38 | } 39 | -------------------------------------------------------------------------------- /src/types/github.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * GitHub API types for PR information extraction 3 | */ 4 | 5 | export interface PullRequestInfo { 6 | number: number; 7 | title: string; 8 | body: string | null; 9 | state: string; 10 | user: { 11 | login: string; 12 | }; 13 | head: { 14 | ref: string; 15 | sha: string; 16 | repo: { 17 | full_name: string; 18 | name: string; 19 | owner: { 20 | login: string; 21 | }; 22 | }; 23 | }; 24 | base: { 25 | ref: string; 26 | sha: string; 27 | repo: { 28 | full_name: string; 29 | name: string; 30 | owner: { 31 | login: string; 32 | }; 33 | }; 34 | }; 35 | } 36 | 37 | export interface PullRequestFile { 38 | filename: string; 39 | status: string; 40 | additions: number; 41 | deletions: number; 42 | changes: number; 43 | } 44 | 45 | export interface PullRequestDiff { 46 | content: string; 47 | size: number; 48 | truncated: boolean; 49 | } 50 | -------------------------------------------------------------------------------- /src/types/inputs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Action input types for the Augment Agent with template support 3 | */ 4 | 5 | export interface InputField { 6 | envVar: string; 7 | required: boolean; 8 | transform?: (value: string) => any; 9 | } 10 | 11 | export interface ActionInputs { 12 | // Base inputs 13 | augmentSessionAuth?: string | undefined; 14 | augmentApiToken?: string | undefined; 15 | augmentApiUrl?: string | undefined; 16 | githubToken?: string | undefined; 17 | instruction?: string | undefined; 18 | instructionFile?: string | undefined; 19 | model?: string | undefined; 20 | 21 | // Template inputs 22 | templateDirectory?: string | undefined; 23 | templateName?: string | undefined; 24 | customContext?: string | undefined; 25 | pullNumber?: number | undefined; 26 | repoName?: string | undefined; 27 | 28 | // Additional configuration inputs 29 | rules?: string[] | undefined; 30 | mcpConfigs?: string[] | undefined; 31 | } 32 | 33 | export interface RepoInfo { 34 | owner: string; 35 | repo: string; 36 | } 37 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build outputs 5 | dist/ 6 | build/ 7 | 8 | # Logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage/ 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Dependency directories 27 | jspm_packages/ 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # Output of 'npm pack' 36 | *.tgz 37 | 38 | # Yarn Integrity file 39 | .yarn-integrity 40 | 41 | # dotenv environment variables file 42 | .env 43 | 44 | # IDE files 45 | .idea/ 46 | *.swp 47 | *.swo 48 | *~ 49 | 50 | # OS generated files 51 | .DS_Store 52 | .DS_Store? 53 | ._* 54 | .Spotlight-V100 55 | .Trashes 56 | ehthumbs.db 57 | Thumbs.db 58 | 59 | # Lock files (don't format these) 60 | bun.lock 61 | bun.lockb 62 | package-lock.json 63 | yarn.lock 64 | 65 | # Git files 66 | .git/ 67 | .gitignore 68 | -------------------------------------------------------------------------------- /.github/workflows/format-check.yml: -------------------------------------------------------------------------------- 1 | name: Format Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize] 9 | branches: 10 | - main 11 | 12 | jobs: 13 | format-check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | 19 | - name: Setup Bun 20 | uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 21 | with: 22 | bun-version: latest 23 | 24 | - name: Install dependencies 25 | run: bun install --frozen-lockfile 26 | 27 | - name: Check formatting 28 | run: bun run format:check 29 | 30 | - name: Check if files would change 31 | run: | 32 | if ! git diff --exit-code; then 33 | echo "❌ Files are not properly formatted. Run 'bun run format' to fix." 34 | exit 1 35 | else 36 | echo "✅ All files are properly formatted." 37 | fi 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Augment Code 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/on-demand-review.yml: -------------------------------------------------------------------------------- 1 | # On-Demand PR Review 2 | # Generates reviews when the "augment_review" label is added to a PR 3 | 4 | name: On-Demand PR Review 5 | on: 6 | pull_request: 7 | types: [labeled] 8 | 9 | jobs: 10 | review: 11 | # Only run when the specific label is added 12 | if: github.event.label.name == 'augment_review' 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | pull-requests: write 17 | steps: 18 | - name: Generate PR Review 19 | uses: augmentcode/review-pr@v0 20 | with: 21 | augment_session_auth: ${{ secrets.AUGMENT_SESSION_AUTH }} 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | pull_number: ${{ github.event.pull_request.number }} 24 | repo_name: ${{ github.repository }} 25 | 26 | - name: Remove trigger label 27 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 28 | with: 29 | script: | 30 | // Remove the trigger label after processing 31 | await github.rest.issues.removeLabel({ 32 | owner: context.repo.owner, 33 | repo: context.repo.repo, 34 | issue_number: context.issue.number, 35 | name: 'augment_review' 36 | }); 37 | 38 | - name: Add completion label 39 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 40 | with: 41 | script: | 42 | // Add a label to indicate the review was generated 43 | await github.rest.issues.addLabels({ 44 | owner: context.repo.owner, 45 | repo: context.repo.repo, 46 | issue_number: context.issue.number, 47 | labels: ['auto-reviewed'] 48 | }); 49 | -------------------------------------------------------------------------------- /.github/workflows/on-demand-description.yml: -------------------------------------------------------------------------------- 1 | # On-Demand PR Description 2 | # Generates descriptions when the "augment_describe" label is added to a PR 3 | 4 | name: On-Demand PR Description 5 | on: 6 | pull_request: 7 | types: [labeled] 8 | 9 | jobs: 10 | describe: 11 | # Only run when the specific label is added 12 | if: github.event.label.name == 'augment_describe' 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | pull-requests: write 17 | steps: 18 | - name: Generate PR Description 19 | uses: augmentcode/describe-pr@v0 20 | with: 21 | augment_session_auth: ${{ secrets.AUGMENT_SESSION_AUTH }} 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | pull_number: ${{ github.event.pull_request.number }} 24 | repo_name: ${{ github.repository }} 25 | 26 | - name: Remove trigger label 27 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 28 | with: 29 | script: | 30 | // Remove the trigger label after processing 31 | await github.rest.issues.removeLabel({ 32 | owner: context.repo.owner, 33 | repo: context.repo.repo, 34 | issue_number: context.issue.number, 35 | name: 'augment_describe' 36 | }); 37 | 38 | - name: Add completion label 39 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 40 | with: 41 | script: | 42 | // Add a label to indicate the description was generated 43 | await github.rest.issues.addLabels({ 44 | owner: context.repo.owner, 45 | repo: context.repo.repo, 46 | issue_number: context.issue.number, 47 | labels: ['auto-described'] 48 | }); 49 | -------------------------------------------------------------------------------- /src/template/extractors/custom-context-extractor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom context extractor - handles parsing JSON strings into context objects 3 | */ 4 | 5 | import { BaseExtractor } from './base-extractor.js'; 6 | import { logger } from '../../utils/logger.js'; 7 | import { ERROR } from '../../config/constants.js'; 8 | import type { ActionInputs } from '../../types/inputs.js'; 9 | 10 | export class CustomContextExtractor extends BaseExtractor> { 11 | constructor() { 12 | super('Custom Context'); 13 | } 14 | 15 | /** 16 | * Creates a CustomContextExtractor instance 17 | */ 18 | static create(_inputs: ActionInputs): CustomContextExtractor { 19 | return new CustomContextExtractor(); 20 | } 21 | 22 | /** 23 | * Determines if custom context extraction should be performed 24 | */ 25 | shouldExtract(inputs: ActionInputs): boolean { 26 | return !!(inputs.customContext && inputs.customContext.trim().length > 0); 27 | } 28 | 29 | /** 30 | * Performs custom context extraction by parsing JSON string 31 | */ 32 | protected performExtraction(inputs: ActionInputs): Record { 33 | const jsonString = inputs.customContext!; 34 | try { 35 | const parsed = JSON.parse(jsonString); 36 | 37 | if (typeof parsed !== 'object' || parsed === null) { 38 | throw new Error('Parsed JSON must be an object'); 39 | } 40 | 41 | logger.debug('Custom context parsed successfully', { 42 | keys: Object.keys(parsed), 43 | keyCount: Object.keys(parsed).length, 44 | }); 45 | 46 | return parsed; 47 | } catch (error) { 48 | const message = `${ERROR.INPUT.INVALID_CONTEXT_JSON}: ${ 49 | error instanceof Error ? error.message : String(error) 50 | }`; 51 | logger.error('Failed to parse custom context JSON', error); 52 | throw new Error(message); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/template/extractors/base-extractor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base extractor class that defines the common interface for all data extractors 3 | */ 4 | 5 | import { logger } from '../../utils/logger.js'; 6 | import type { ActionInputs } from '../../types/inputs.js'; 7 | 8 | export abstract class BaseExtractor { 9 | protected readonly extractorName: string; 10 | 11 | constructor(extractorName: string) { 12 | this.extractorName = extractorName; 13 | } 14 | 15 | /** 16 | * Static factory method that each extractor must implement 17 | * Creates an instance of the extractor configured for the given inputs 18 | */ 19 | static create(_inputs: ActionInputs): BaseExtractor { 20 | throw new Error('Subclasses must implement the static create method'); 21 | } 22 | 23 | /** 24 | * Determines if extraction should be performed based on the action inputs 25 | */ 26 | abstract shouldExtract(inputs: ActionInputs): boolean; 27 | 28 | /** 29 | * Performs the actual data extraction 30 | */ 31 | protected abstract performExtraction(inputs: ActionInputs): Promise | TOutput; 32 | 33 | /** 34 | * Main extraction method that handles logging and error handling 35 | */ 36 | async extract(inputs: ActionInputs): Promise { 37 | try { 38 | if (!this.shouldExtract(inputs)) { 39 | logger.debug(`${this.extractorName} extraction skipped`, { 40 | reason: 'shouldExtract returned false', 41 | }); 42 | return undefined; 43 | } 44 | 45 | logger.debug(`Starting ${this.extractorName} extraction`); 46 | const result = await this.performExtraction(inputs); 47 | 48 | logger.debug(`${this.extractorName} extraction completed successfully`); 49 | return result; 50 | } catch (error) { 51 | logger.error(`${this.extractorName} extraction failed`, error); 52 | throw error; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "augment-agent", 3 | "version": "0.1.3", 4 | "description": "AI-powered code assistance for GitHub Actions using Augment's intelligent development tools", 5 | "main": "action.yml", 6 | "type": "module", 7 | "scripts": { 8 | "test": "node --test", 9 | "typecheck": "tsc --noEmit", 10 | "format": "prettier --write .", 11 | "format:check": "prettier --check .", 12 | "format:staged": "prettier --write --cache --ignore-unknown", 13 | "lint": "prettier --check .", 14 | "lint:fix": "prettier --write .", 15 | "dev": "node --watch src/index.ts", 16 | "version:patch": "npm version patch && git push && git push --tags", 17 | "version:minor": "npm version minor && git push && git push --tags", 18 | "version:major": "npm version major && git push && git push --tags" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/augmentcode/augment-agent.git" 23 | }, 24 | "keywords": [ 25 | "github-action", 26 | "ai", 27 | "code-review", 28 | "pull-request", 29 | "automation", 30 | "augment", 31 | "developer-tools", 32 | "ci-cd" 33 | ], 34 | "author": "Augment Code", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/augmentcode/augment-agent/issues" 38 | }, 39 | "homepage": "https://github.com/augmentcode/augment-agent#readme", 40 | "engines": { 41 | "node": ">=22.0.0" 42 | }, 43 | "files": [ 44 | "action.yml", 45 | "src/", 46 | "README.md", 47 | "LICENSE" 48 | ], 49 | "dependencies": { 50 | "@actions/core": "^1.10.1", 51 | "@octokit/rest": "^20.0.2", 52 | "nunjucks": "^3.2.4", 53 | "yaml": "^2.3.4", 54 | "zod": "^3.22.4" 55 | }, 56 | "devDependencies": { 57 | "@types/node": "^22.16.4", 58 | "@types/nunjucks": "^3.2.6", 59 | "prettier": "^3.6.2", 60 | "tsx": "^4.20.6", 61 | "typescript": "^5.8.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example-workflows/pr-description.yml: -------------------------------------------------------------------------------- 1 | name: Augment Agent - PR Description 2 | on: 3 | pull_request: 4 | types: [opened] 5 | 6 | jobs: 7 | generate-pr-description: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | - name: Create instruction file 14 | env: 15 | PR_NUMBER: ${{ github.event.pull_request.number }} 16 | REPOSITORY: ${{ github.repository }} 17 | BASE_BRANCH: ${{ github.event.pull_request.base.ref }} 18 | HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} 19 | run: | 20 | cat > /tmp/pr-instruction.txt << EOF 21 | Analyze the following pull request and generate a comprehensive PR description: 22 | 23 | **Pull Request Information:** 24 | - PR Number: ${PR_NUMBER} 25 | - Repository: ${REPOSITORY} 26 | - Base Branch: ${BASE_BRANCH} 27 | - Head Branch: ${HEAD_BRANCH} 28 | 29 | **Task:** 30 | Please analyze the code changes in this pull request and generate a comprehensive PR description including: 31 | - A clear summary of what was changed 32 | - The purpose and motivation for the changes 33 | - Key modifications made to specific files 34 | - Any testing considerations or notes for reviewers 35 | - Breaking changes or migration notes if applicable 36 | 37 | Focus on the actual code changes and provide a description that would help reviewers understand the scope and impact of this PR. 38 | 39 | Once you're done, please post the description as a comment on the PR. 40 | EOF 41 | - name: Generate PR Description 42 | uses: augmentcode/augment-agent@v0 43 | with: 44 | augment_session_auth: ${{ secrets.AUGMENT_SESSION_AUTH }} 45 | github_token: ${{ secrets.GITHUB_TOKEN }} 46 | instruction_file: /tmp/pr-instruction.txt 47 | -------------------------------------------------------------------------------- /example-workflows/code-review.yml: -------------------------------------------------------------------------------- 1 | name: Augment Agent - Code Review 2 | on: 3 | pull_request: 4 | types: [opened] 5 | 6 | jobs: 7 | code-review: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | - name: Create instruction file 14 | env: 15 | PR_NUMBER: ${{ github.event.pull_request.number }} 16 | REPOSITORY: ${{ github.repository }} 17 | BASE_BRANCH: ${{ github.event.pull_request.base.ref }} 18 | HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} 19 | run: | 20 | cat > /tmp/review-instruction.txt << EOF 21 | Perform a comprehensive code review of the following pull request: 22 | 23 | **Pull Request Information:** 24 | - PR Number: ${PR_NUMBER} 25 | - Repository: ${REPOSITORY} 26 | - Base Branch: ${BASE_BRANCH} 27 | - Head Branch: ${HEAD_BRANCH} 28 | 29 | **Review Focus:** 30 | Analyze the modified files and provide detailed feedback on: 31 | - Code quality and adherence to best practices 32 | - Potential bugs, errors, or security vulnerabilities 33 | - Performance implications of the changes 34 | - Suggestions for improvement or optimization 35 | - Any missing error handling or edge cases 36 | - Code maintainability and readability 37 | 38 | Please provide specific, actionable feedback with file and line references where applicable. 39 | Focus on the actual code changes and their impact on the codebase. 40 | 41 | Please post your review as a review comment on the PR. Do not approve or request changes. 42 | EOF 43 | - name: Code Review 44 | uses: augmentcode/augment-agent@v0 45 | with: 46 | augment_session_auth: ${{ secrets.AUGMENT_SESSION_AUTH }} 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | instruction_file: /tmp/review-instruction.txt 49 | -------------------------------------------------------------------------------- /src/template/context-builder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Context builder - responsible for aggregating context from multiple sources 3 | */ 4 | 5 | import { logger } from '../utils/logger.js'; 6 | import { PRExtractor } from './extractors/pr-extractor.js'; 7 | import { CustomContextExtractor } from './extractors/custom-context-extractor.js'; 8 | import type { TemplateContext } from '../types/context.js'; 9 | import type { ActionInputs } from '../types/inputs.js'; 10 | 11 | export class ContextBuilder { 12 | constructor( 13 | private prExtractor: PRExtractor, 14 | private customContextExtractor: CustomContextExtractor 15 | ) {} 16 | 17 | static create(inputs: ActionInputs): ContextBuilder { 18 | return new ContextBuilder(PRExtractor.create(inputs), CustomContextExtractor.create(inputs)); 19 | } 20 | 21 | async buildContext(inputs: ActionInputs): Promise { 22 | try { 23 | logger.debug('Building template context from inputs'); 24 | 25 | const context: TemplateContext = {}; 26 | 27 | // Extract PR data (extractor decides if it should run) 28 | const prData = await this.prExtractor.extract(inputs); 29 | if (prData) { 30 | context.pr = prData; 31 | logger.debug('PR data extracted and added to context', { 32 | prNumber: prData.number, 33 | title: prData.title, 34 | }); 35 | } 36 | 37 | // Extract custom context if available 38 | const customContext = await this.customContextExtractor.extract(inputs); 39 | if (customContext) { 40 | context.custom = customContext; 41 | logger.debug('Custom context extracted and added', { 42 | customContextKeys: Object.keys(customContext), 43 | }); 44 | } 45 | 46 | logger.info('Template context built successfully', { 47 | hasPR: !!context.pr, 48 | hasCustom: !!context.custom, 49 | customKeys: context.custom ? Object.keys(context.custom) : [], 50 | }); 51 | 52 | return context; 53 | } catch (error) { 54 | logger.error('Failed to build template context', error); 55 | throw error; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/template/template-processor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Template processor - orchestrates the entire template processing pipeline 3 | */ 4 | 5 | import { TemplateEngine } from './template-engine.js'; 6 | import { ContextBuilder } from './context-builder.js'; 7 | import { FileUtils } from '../utils/file-utils.js'; 8 | import { logger } from '../utils/logger.js'; 9 | import { PATHS } from '../config/constants.js'; 10 | import type { ActionInputs } from '../types/inputs.js'; 11 | 12 | export class TemplateProcessor { 13 | constructor( 14 | private templateEngine: TemplateEngine, 15 | private contextBuilder: ContextBuilder 16 | ) {} 17 | 18 | static create(inputs: ActionInputs): TemplateProcessor { 19 | return new TemplateProcessor(TemplateEngine.create(inputs), ContextBuilder.create(inputs)); 20 | } 21 | 22 | async processTemplate(inputs: ActionInputs): Promise { 23 | try { 24 | logger.info('Starting template processing pipeline', { 25 | templateDirectory: inputs.templateDirectory, 26 | templateName: inputs.templateName, 27 | }); 28 | 29 | const context = await this.contextBuilder.buildContext(inputs); 30 | const content = await this.templateEngine.renderTemplate(inputs.templateName!, context); 31 | const instructionFilePath = await this.writeInstructionFile(content); 32 | 33 | logger.info('Template processing pipeline completed', { 34 | instructionFile: instructionFilePath, 35 | contentLength: content.length, 36 | }); 37 | 38 | return instructionFilePath; 39 | } catch (error) { 40 | logger.error('Template processing pipeline failed', error); 41 | throw error; 42 | } 43 | } 44 | 45 | private async writeInstructionFile(content: string): Promise { 46 | try { 47 | await FileUtils.ensureDirectoryExists(PATHS.TEMP_DIR); 48 | await FileUtils.writeFile(PATHS.INSTRUCTION_FILE, content); 49 | 50 | logger.info('Instruction file written', { 51 | filePath: PATHS.INSTRUCTION_FILE, 52 | contentLength: content.length, 53 | }); 54 | 55 | return PATHS.INSTRUCTION_FILE; 56 | } catch (error) { 57 | logger.error('Failed to write instruction file', error); 58 | throw error; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # TypeScript build info (but keep dist/ for GitHub Actions) 8 | *.tsbuildinfo 9 | 10 | # TypeScript compiled output in src/ (we use tsx to run directly) 11 | src/**/*.js 12 | src/**/*.d.ts 13 | src/**/*.js.map 14 | src/**/*.d.ts.map 15 | 16 | # Test coverage 17 | coverage/ 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage/ 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | .env.local 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | public 91 | 92 | # Storybook build outputs 93 | .out 94 | .storybook-out 95 | 96 | # Temporary folders 97 | tmp/ 98 | temp/ 99 | 100 | # Logs 101 | logs 102 | *.log 103 | 104 | # Editor directories and files 105 | .vscode/ 106 | .idea/ 107 | *.swp 108 | *.swo 109 | *~ 110 | 111 | # OS generated files 112 | .DS_Store 113 | .DS_Store? 114 | ._* 115 | .Spotlight-V100 116 | .Trashes 117 | ehthumbs.db 118 | Thumbs.db 119 | 120 | # Package manager lock files (optional - some teams prefer to commit these) 121 | # Uncomment the next line if you want to ignore lock files 122 | # package-lock.json 123 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Logging utilities for the Augment Agent 3 | */ 4 | 5 | import * as core from '@actions/core'; 6 | import { ACTION_CONFIG } from '../config/constants.js'; 7 | 8 | /** 9 | * Structured logger for GitHub Actions 10 | */ 11 | export class Logger { 12 | private context: string; 13 | 14 | constructor(context: string = ACTION_CONFIG.NAME) { 15 | this.context = context; 16 | } 17 | 18 | /** 19 | * Log debug information 20 | */ 21 | debug(message: string, data?: Record): void { 22 | const logMessage = this.formatMessage(message, data); 23 | core.debug(logMessage); 24 | } 25 | 26 | /** 27 | * Log informational message 28 | */ 29 | info(message: string, data?: Record): void { 30 | const logMessage = this.formatMessage(message, data); 31 | core.info(logMessage); 32 | } 33 | 34 | /** 35 | * Log warning message 36 | */ 37 | warning(message: string, data?: Record): void { 38 | const logMessage = this.formatMessage(message, data); 39 | core.warning(logMessage); 40 | } 41 | 42 | /** 43 | * Log error message 44 | */ 45 | error(message: string, error?: Error | unknown, data?: Record): void { 46 | const errorData = 47 | error instanceof Error 48 | ? { error: error.message, stack: error.stack, ...data } 49 | : { error: String(error), ...data }; 50 | 51 | const logMessage = this.formatMessage(message, errorData); 52 | core.error(logMessage); 53 | } 54 | 55 | /** 56 | * Set a failed status with error message 57 | */ 58 | setFailed(message: string, error?: Error | unknown): void { 59 | if (error instanceof Error) { 60 | this.error(message, error); 61 | core.setFailed(`${message}: ${error.message}`); 62 | } else { 63 | this.error(message, error); 64 | core.setFailed(message); 65 | } 66 | } 67 | 68 | /** 69 | * Format log message with context and optional data 70 | */ 71 | private formatMessage(message: string, data?: Record): string { 72 | const timestamp = new Date().toISOString(); 73 | let formattedMessage = `[${timestamp}] [${this.context}] ${message}`; 74 | 75 | if (data && Object.keys(data).length > 0) { 76 | formattedMessage += ` | Data: ${JSON.stringify(data, null, 2)}`; 77 | } 78 | 79 | return formattedMessage; 80 | } 81 | } 82 | 83 | /** 84 | * Default logger instance 85 | */ 86 | export const logger = new Logger(); 87 | -------------------------------------------------------------------------------- /src/config/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration constants for the Augment Agent 3 | */ 4 | 5 | import type { InputField } from '../types/inputs.js'; 6 | 7 | export const ACTION_CONFIG = { 8 | NAME: 'Augment Agent', 9 | }; 10 | 11 | export const INPUT_FIELD_MAP: Record = { 12 | augmentSessionAuth: { envVar: 'INPUT_AUGMENT_SESSION_AUTH', required: false }, 13 | augmentApiToken: { envVar: 'INPUT_AUGMENT_API_TOKEN', required: false }, 14 | augmentApiUrl: { envVar: 'INPUT_AUGMENT_API_URL', required: false }, 15 | customContext: { envVar: 'INPUT_CUSTOM_CONTEXT', required: false }, 16 | githubToken: { envVar: 'INPUT_GITHUB_TOKEN', required: false }, 17 | instruction: { envVar: 'INPUT_INSTRUCTION', required: false }, 18 | instructionFile: { envVar: 'INPUT_INSTRUCTION_FILE', required: false }, 19 | pullNumber: { 20 | envVar: 'INPUT_PULL_NUMBER', 21 | required: false, 22 | transform: (val: string) => parseInt(val, 10), 23 | }, 24 | repoName: { envVar: 'INPUT_REPO_NAME', required: false }, 25 | templateDirectory: { envVar: 'INPUT_TEMPLATE_DIRECTORY', required: false }, 26 | templateName: { envVar: 'INPUT_TEMPLATE_NAME', required: false }, 27 | model: { envVar: 'INPUT_MODEL', required: false }, 28 | rules: { envVar: 'INPUT_RULES', required: false }, 29 | mcpConfigs: { envVar: 'INPUT_MCP_CONFIGS', required: false }, 30 | }; 31 | 32 | export const TEMPLATE_CONFIG = { 33 | DEFAULT_TEMPLATE_NAME: 'prompt.njk', 34 | MAX_TEMPLATE_SIZE: 128 * 1024, // 128KB 35 | MAX_DIFF_SIZE: 128 * 1024, // 128KB 36 | }; 37 | 38 | export const PATHS = { 39 | TEMP_DIR: '/tmp', 40 | DIFF_FILE_PATTERN: 'pr-{pullNumber}-diff.patch', 41 | INSTRUCTION_FILE: '/tmp/generated-instruction.txt', 42 | }; 43 | 44 | export const ERROR = { 45 | GITHUB: { 46 | API_ERROR: 'GitHub API request failed', 47 | }, 48 | INPUT: { 49 | CONFLICTING_INSTRUCTION_INPUTS: 'Cannot specify both instruction and instruction_file', 50 | CONFLICTING_INSTRUCTION_TEMPLATE: 51 | 'Cannot use both instruction inputs and template inputs simultaneously', 52 | INVALID: 'Invalid action inputs', 53 | INVALID_CONTEXT_JSON: 'Invalid JSON in custom_context', 54 | MISMATCHED_PR_FIELDS: 'Both pull_number and repo_name are required for PR extraction', 55 | MISSING_INSTRUCTION_OR_TEMPLATE: 56 | 'Either instruction/instruction_file or template_directory must be provided', 57 | REPO_FORMAT: 'Repository name must be in format "owner/repo"', 58 | }, 59 | TEMPLATE: { 60 | MISSING_DIRECTORY: 'template_directory is required when using templates', 61 | NOT_FOUND: 'Template file not found', 62 | PATH_OUTSIDE_DIRECTORY: 'Template path is outside template directory', 63 | RENDER_ERROR: 'Failed to render template', 64 | TOO_LARGE: 'Template file exceeds maximum size', 65 | }, 66 | } as const; 67 | -------------------------------------------------------------------------------- /example-workflows/triage-issue.yml: -------------------------------------------------------------------------------- 1 | name: Augment Agent - Issue Triage 2 | on: 3 | issues: 4 | types: [opened] 5 | 6 | jobs: 7 | triage-issue: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | - name: Create instruction file 14 | env: 15 | ISSUE_NUMBER: ${{ github.event.issue.number }} 16 | REPOSITORY: ${{ github.repository }} 17 | ISSUE_TITLE: ${{ github.event.issue.title }} 18 | ISSUE_AUTHOR: ${{ github.event.issue.user.login }} 19 | CURRENT_LABELS: ${{ join(github.event.issue.labels.*.name, ', ') }} 20 | run: | 21 | cat > /tmp/triage-instruction.txt << EOF 22 | Analyze the following GitHub issue and provide comprehensive triage recommendations: 23 | 24 | **Issue Information:** 25 | - Issue Number: ${ISSUE_NUMBER} 26 | - Repository: ${REPOSITORY} 27 | - Title: ${ISSUE_TITLE} 28 | - Author: ${ISSUE_AUTHOR} 29 | - Current Labels: ${CURRENT_LABELS} 30 | 31 | **Triage Analysis:** 32 | Please analyze this issue and provide detailed triage recommendations including: 33 | - **Priority Level**: Assess urgency and impact (Critical/High/Medium/Low) 34 | - **Issue Type**: Categorize the issue (bug, feature request, enhancement, documentation, question, etc.) 35 | - **Complexity Estimate**: Evaluate implementation complexity (Simple/Medium/Complex) 36 | - **Suggested Labels**: Recommend appropriate labels for categorization 37 | - **Team/Component**: Identify which team or component this issue relates to 38 | - **Dependencies**: Note any dependencies or blockers mentioned 39 | - **Reproducibility**: For bugs, assess if reproduction steps are clear 40 | - **Acceptance Criteria**: Identify if requirements are well-defined 41 | - **Similar Issues**: Note if this appears related to existing issues 42 | 43 | **Recommendations:** 44 | - Suggest next steps for handling this issue 45 | - Recommend if additional information is needed from the reporter 46 | - Identify potential assignees or teams based on the issue content 47 | - Note any immediate actions that should be taken 48 | 49 | Focus on providing actionable insights that will help maintainers efficiently process and prioritize this issue. 50 | 51 | Please post your triage analysis as a comment on the issue. 52 | EOF 53 | - name: Triage Issue 54 | uses: augmentcode/augment-agent@v0 55 | with: 56 | augment_session_auth: ${{ secrets.AUGMENT_SESSION_AUTH }} 57 | github_token: ${{ secrets.GITHUB_TOKEN }} 58 | instruction_file: /tmp/triage-instruction.txt 59 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | This document outlines the recommended release process for the Augment Agent GitHub Action. 4 | 5 | ## Quick Release (Recommended) 6 | 7 | Use the built-in npm scripts for easy version bumping: 8 | 9 | ```bash 10 | # For patch releases (bug fixes): 0.1.0 -> 0.1.1 11 | npm run version:patch 12 | 13 | # For minor releases (new features): 0.1.0 -> 0.2.0 14 | npm run version:minor 15 | 16 | # For major releases (breaking changes): 0.1.0 -> 1.0.0 17 | npm run version:major 18 | ``` 19 | 20 | These scripts will: 21 | 22 | 1. Update the version in `package.json` 23 | 2. Create a git tag (e.g., `v0.1.1`) 24 | 3. Push the changes and tag to GitHub 25 | 4. Trigger the automated release workflow 26 | 27 | ## Manual Release Process 28 | 29 | If you prefer manual control: 30 | 31 | 1. **Update the version in package.json:** 32 | 33 | ```bash 34 | npm version 0.1.1 # or whatever version you want 35 | ``` 36 | 37 | 2. **Type check the project:** 38 | 39 | ```bash 40 | npm run typecheck 41 | ``` 42 | 43 | 3. **Commit the changes:** 44 | 45 | ```bash 46 | git add package.json 47 | git commit -m "chore: release v0.1.1" 48 | ``` 49 | 50 | 4. **Create and push the tag:** 51 | ```bash 52 | git tag v0.1.1 53 | git push origin main 54 | git push origin v0.1.1 55 | ``` 56 | 57 | ## What Happens During Release 58 | 59 | When a tag is pushed, the GitHub workflow will: 60 | 61 | 1. ✅ Verify the package.json version matches the tag 62 | 2. 🔍 Type check the project to ensure it compiles 63 | 3. 🏷️ Update the major version tag (e.g., `v1` for `v1.2.3`) 64 | 4. 📦 Create a GitHub release with usage examples 65 | 5. 🚀 Make the release available for users 66 | 67 | ## Version Strategy 68 | 69 | - **Patch** (0.0.X): Bug fixes, documentation updates 70 | - **Minor** (0.X.0): New features, backwards compatible 71 | - **Major** (X.0.0): Breaking changes 72 | 73 | ## Major Version Tags 74 | 75 | The release process automatically maintains major version tags: 76 | 77 | - `v0.1.5` creates/updates `v0` 78 | - `v1.2.3` creates/updates `v1` 79 | - `v2.0.0` creates/updates `v2` 80 | 81 | This allows users to pin to major versions for automatic updates: 82 | 83 | ```yaml 84 | - uses: augmentcode/augment-agent@v1 # Gets latest v1.x.x 85 | ``` 86 | 87 | ## Troubleshooting 88 | 89 | **Version mismatch error:** 90 | 91 | - Ensure package.json version matches the git tag version 92 | - The tag should be `v` + the package.json version (e.g., `v0.1.1`) 93 | 94 | **Type check failures:** 95 | 96 | - Run `npm run typecheck` locally to check for TypeScript errors 97 | - Ensure all dependencies are properly installed 98 | 99 | **Release not created:** 100 | 101 | - Check the GitHub Actions logs for detailed error messages 102 | - Verify the GITHUB_TOKEN has proper permissions 103 | -------------------------------------------------------------------------------- /example-workflows/README.md: -------------------------------------------------------------------------------- 1 | # Example Workflows 2 | 3 | This directory contains example GitHub Actions workflows demonstrating different use cases for the Augment Agent. 4 | 5 | **Important**: These workflows pass essential PR context information to the Augment Agent through instruction files that include the PR number, repository, base branch, and head branch. This provides the agent with the necessary context to identify and analyze the specific pull request changes. 6 | 7 | ## Available Examples 8 | 9 | ### [pr-description.yml](./pr-description.yml) 10 | 11 | Automatically generates comprehensive PR descriptions when a PR is first opened. Creates a dynamic instruction file with essential PR context (number, repository, base/head branches). 12 | 13 | ### [code-review.yml](./code-review.yml) 14 | 15 | Performs automated code review when a PR is first opened. Creates an instruction file that includes: 16 | 17 | - Essential PR identification information 18 | - Focus areas for code quality and best practices 19 | - Security vulnerability detection 20 | - Performance and maintainability analysis 21 | 22 | ### [triage-issue.yml](./triage-issue.yml) 23 | 24 | Automatically triages GitHub issues when they are opened, edited, or labeled. Provides comprehensive analysis including: 25 | 26 | - Priority and complexity assessment 27 | - Issue categorization and suggested labels 28 | - Team/component identification 29 | - Reproducibility and requirements evaluation 30 | - Actionable recommendations for maintainers 31 | 32 | ### [template-pr-review.yml](./template-pr-review.yml) 33 | 34 | Demonstrates the **template system** for dynamic instruction generation. This example shows how to: 35 | 36 | - Use Nunjucks templates for flexible instruction creation 37 | - Automatically extract PR context (files, diffs, metadata) 38 | - Include custom context data via JSON 39 | - Apply conditional logic based on PR characteristics 40 | - Format file lists and read diff content within templates 41 | 42 | This template-based approach is ideal for complex, reusable workflows that need to adapt based on PR content. 43 | 44 | ## Customization 45 | 46 | You can modify these examples by: 47 | 48 | - **Simple workflows**: Modifying the instruction file content to focus on different aspects 49 | - **Template workflows**: Creating custom Nunjucks templates for dynamic, reusable instructions (see [template-pr-review.yml](./template-pr-review.yml)) 50 | - Changing the trigger events (e.g., add `synchronize` to run on updates, or filter by specific branches) 51 | - Adding additional PR context information (labels, reviewers, etc.) 52 | - Including file diff information or specific file paths 53 | - Combining multiple steps in a single workflow 54 | - Adding conditional logic based on file changes or PR labels 55 | - Customizing the instruction file location and naming 56 | 57 | **Note**: These examples only run when a PR is first opened (`types: [opened]`) to avoid redundant analysis on every update. Add `synchronize` to the types array if you want the workflow to run on every push to the PR. 58 | 59 | For template system documentation, see [TEMPLATE.md](../TEMPLATE.md). 60 | For more advanced usage patterns, see the main [README.md](../README.md) documentation. 61 | -------------------------------------------------------------------------------- /src/utils/file-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File system utilities 3 | */ 4 | 5 | import { promises as fs } from 'node:fs'; 6 | import { resolve } from 'node:path'; 7 | import { TEMPLATE_CONFIG, ERROR } from '../config/constants.js'; 8 | import { logger } from './logger.js'; 9 | 10 | export class FileUtils { 11 | static async writeFile(filePath: string, content: string): Promise { 12 | try { 13 | logger.debug('Writing file', { filePath, contentLength: content.length }); 14 | await fs.writeFile(filePath, content, 'utf8'); 15 | logger.debug('File written successfully', { filePath }); 16 | } catch (error) { 17 | logger.error('Failed to write file', error, { filePath }); 18 | throw error; 19 | } 20 | } 21 | 22 | static async readFile(filePath: string): Promise { 23 | try { 24 | logger.debug('Reading file', { filePath }); 25 | const content = await fs.readFile(filePath, 'utf8'); 26 | logger.debug('File read successfully', { filePath, contentLength: content.length }); 27 | return content; 28 | } catch (error) { 29 | logger.error('Failed to read file', error, { filePath }); 30 | throw error; 31 | } 32 | } 33 | 34 | static async validateTemplateFile(templateDir: string, templateName: string): Promise { 35 | logger.debug('Validating template file', { templateDir, templateName }); 36 | 37 | const templatePath = resolve(templateDir, templateName); 38 | 39 | // Security check - ensure template is within template directory 40 | if (!templatePath.startsWith(resolve(templateDir))) { 41 | const message = `${ERROR.TEMPLATE.PATH_OUTSIDE_DIRECTORY}: ${templateName}`; 42 | logger.error(message, undefined, { templateDir, templateName, templatePath }); 43 | throw new Error(message); 44 | } 45 | 46 | try { 47 | await fs.access(templatePath); 48 | logger.debug('Template file exists', { templatePath }); 49 | } catch (error) { 50 | const message = `${ERROR.TEMPLATE.NOT_FOUND}: ${templatePath}`; 51 | logger.error(message, error, { templateDir, templateName, templatePath }); 52 | throw new Error(message); 53 | } 54 | 55 | // Check file size 56 | const stats = await fs.stat(templatePath); 57 | if (stats.size > TEMPLATE_CONFIG.MAX_TEMPLATE_SIZE) { 58 | const message = `${ERROR.TEMPLATE.TOO_LARGE}: ${stats.size} bytes`; 59 | logger.error(message, undefined, { 60 | templatePath, 61 | fileSize: stats.size, 62 | maxSize: TEMPLATE_CONFIG.MAX_TEMPLATE_SIZE, 63 | }); 64 | throw new Error(message); 65 | } 66 | 67 | logger.debug('Template file validation successful', { 68 | templatePath, 69 | fileSize: stats.size, 70 | }); 71 | return templatePath; 72 | } 73 | 74 | static async ensureDirectoryExists(dirPath: string): Promise { 75 | try { 76 | logger.debug('Ensuring directory exists', { dirPath }); 77 | await fs.mkdir(dirPath, { recursive: true }); 78 | logger.debug('Directory ensured', { dirPath }); 79 | } catch (error) { 80 | // Ignore error if directory already exists 81 | if ((error as any).code !== 'EEXIST') { 82 | logger.error('Failed to create directory', error, { dirPath }); 83 | throw error; 84 | } 85 | logger.debug('Directory already exists', { dirPath }); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example-workflows/template-pr-review.yml: -------------------------------------------------------------------------------- 1 | name: Augment Agent - Template-Based PR Review 2 | on: 3 | pull_request: 4 | types: [opened, synchronize] 5 | 6 | jobs: 7 | template-review: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | 14 | - name: Create template directory 15 | run: | 16 | mkdir -p .github/templates 17 | 18 | - name: Create PR review template 19 | run: | 20 | cat > .github/templates/pr-review.njk << 'EOF' 21 | Please review pull request #{{ pr.number }}: "{{ pr.title }}" 22 | 23 | **Author:** {{ pr.author }} 24 | **Branch:** {{ pr.head.ref }} → {{ pr.base.ref }} 25 | **Repository:** {{ pr.base.repo.full_name }} 26 | 27 | {% if pr.body %} 28 | **Description:** 29 | {{ pr.body }} 30 | {% endif %} 31 | 32 | **Changed Files ({{ pr.changed_files_list | length }} total):** 33 | {{ pr.changed_files_list | format_files }} 34 | 35 | {% if pr.changed_files_list | length > 15 %} 36 | ⚠️ This is a large PR with {{ pr.changed_files_list | length }} files changed. Please pay special attention to the scope and impact. 37 | {% endif %} 38 | 39 | {% set has_tests = pr.changed_files | includes("test") or pr.changed_files | includes("spec") %} 40 | {% if not has_tests %} 41 | ⚠️ No test files detected in this PR. Please verify that appropriate tests have been added or updated. 42 | {% endif %} 43 | 44 | **Code Changes:** 45 | ```diff 46 | {{ pr.diff_file | maybe_read_file }} 47 | ``` 48 | 49 | **Review Focus:** 50 | Please provide a thorough code review covering: 51 | 52 | 1. **Code Quality**: Check for adherence to coding standards and best practices 53 | 2. **Security**: Look for potential security vulnerabilities or concerns 54 | 3. **Performance**: Assess any performance implications of the changes 55 | 4. **Maintainability**: Evaluate code readability and maintainability 56 | 5. **Testing**: Verify adequate test coverage for new functionality 57 | 6. **Documentation**: Check if documentation updates are needed 58 | 59 | {% if custom.priority %} 60 | **Priority Level:** {{ custom.priority | upper }} 61 | {% if custom.priority == "high" %} 62 | 🚨 This is a high-priority PR requiring urgent review and approval. 63 | {% endif %} 64 | {% endif %} 65 | 66 | {% if custom.reviewers %} 67 | **Suggested Reviewers:** {{ custom.reviewers | join(", ") }} 68 | {% endif %} 69 | 70 | Please provide specific, actionable feedback with file and line references where applicable. 71 | Focus on the actual code changes and their impact on the codebase. 72 | 73 | After your review, please post your feedback as a review comment on this PR. 74 | EOF 75 | 76 | - name: Template-Based Code Review 77 | uses: augmentcode/augment-agent@v0 78 | with: 79 | augment_session_auth: ${{ secrets.AUGMENT_SESSION_AUTH }} 80 | github_token: ${{ secrets.GITHUB_TOKEN }} 81 | template_directory: ".github/templates" 82 | template_name: "pr-review.njk" 83 | pull_number: ${{ github.event.pull_request.number }} 84 | repo_name: ${{ github.repository }} 85 | custom_context: | 86 | { 87 | "priority": "normal", 88 | "reviewers": ["team-lead", "senior-dev"], 89 | "focus_areas": ["security", "performance"] 90 | } 91 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Augment Agent" 2 | description: "AI-powered code assistance for GitHub pull requests using Augment's intelligent development tools" 3 | author: "Augment Code" 4 | branding: 5 | icon: "zap" 6 | color: "purple" 7 | 8 | inputs: 9 | augment_session_auth: 10 | description: "Augment session authentication JSON. Alternative to augment_api_token and augment_api_url. Store as repository secret for security." 11 | required: false 12 | augment_api_token: 13 | description: "API token for Augment services. Store as repository secret for security. Alternative to augment_session_auth." 14 | required: false 15 | augment_api_url: 16 | description: "URL endpoint for Augment API requests. Store as repository variable. Alternative to augment_session_auth." 17 | required: false 18 | github_token: 19 | description: "GitHub token for API access. Must have 'repo' and 'user:email' scopes." 20 | required: false 21 | instruction: 22 | description: "Direct instruction text for the AI agent. Use this for simple, inline instructions. Mutually exclusive with instruction_file and template inputs." 23 | required: false 24 | instruction_file: 25 | description: "Path to file containing detailed instructions for the AI agent. Use this for complex, multi-line instructions. Mutually exclusive with instruction and template inputs." 26 | required: false 27 | template_directory: 28 | description: "Path to directory containing template files. Use this for template-based instruction generation. Mutually exclusive with instruction and instruction_file." 29 | required: false 30 | template_name: 31 | description: "Name of the template file to use (default: prompt.njk). Only used with template_directory." 32 | required: false 33 | default: "prompt.njk" 34 | pull_number: 35 | description: "Pull request number for extracting PR context. Required when using templates with PR data." 36 | required: false 37 | repo_name: 38 | description: "Repository name in format 'owner/repo'. Required when using templates with PR data." 39 | required: false 40 | custom_context: 41 | description: "Additional context data as JSON string to pass to templates." 42 | required: false 43 | model: 44 | description: "Model to use; forwarded to auggie as --model" 45 | required: false 46 | rules: 47 | description: "JSON array of rules file paths. Each entry is forwarded to auggie as an individual --rules flag." 48 | required: false 49 | mcp_configs: 50 | description: "JSON array of MCP config file paths. Each entry is forwarded to auggie as an individual --mcp-config flag." 51 | required: false 52 | 53 | runs: 54 | using: "composite" 55 | steps: 56 | - name: Setup Node 57 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 58 | with: 59 | node-version: 22 60 | - name: Setup Auggie 61 | run: npm install -g @augmentcode/auggie 62 | shell: bash 63 | - name: Install Action Dependencies 64 | run: npm install --production=false 65 | shell: bash 66 | working-directory: ${{ github.action_path }} 67 | - name: Run Augment Agent 68 | run: npx tsx $GITHUB_ACTION_PATH/src/index.ts 69 | shell: bash 70 | env: 71 | INPUT_AUGMENT_SESSION_AUTH: ${{ inputs.augment_session_auth }} 72 | INPUT_AUGMENT_API_TOKEN: ${{ inputs.augment_api_token }} 73 | INPUT_AUGMENT_API_URL: ${{ inputs.augment_api_url }} 74 | INPUT_GITHUB_TOKEN: ${{ inputs.github_token }} 75 | INPUT_INSTRUCTION: ${{ inputs.instruction }} 76 | INPUT_INSTRUCTION_FILE: ${{ inputs.instruction_file }} 77 | INPUT_TEMPLATE_DIRECTORY: ${{ inputs.template_directory }} 78 | INPUT_TEMPLATE_NAME: ${{ inputs.template_name }} 79 | INPUT_PULL_NUMBER: ${{ inputs.pull_number }} 80 | INPUT_REPO_NAME: ${{ inputs.repo_name }} 81 | INPUT_CUSTOM_CONTEXT: ${{ inputs.custom_context }} 82 | INPUT_MODEL: ${{ inputs.model }} 83 | INPUT_RULES: ${{ inputs.rules }} 84 | INPUT_MCP_CONFIGS: ${{ inputs.mcp_configs }} 85 | -------------------------------------------------------------------------------- /src/template/template-engine.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Template engine for rendering Nunjucks templates 3 | */ 4 | 5 | import nunjucks from 'nunjucks'; 6 | import { resolve } from 'node:path'; 7 | import { FileUtils } from '../utils/file-utils.js'; 8 | import { logger } from '../utils/logger.js'; 9 | import { ERROR, PATHS } from '../config/constants.js'; 10 | import type { TemplateContext } from '../types/context.js'; 11 | import { ActionInputs } from '../types/inputs.js'; 12 | 13 | export class TemplateEngine { 14 | private env: nunjucks.Environment; 15 | private templateDirectory: string; 16 | 17 | constructor(templateDirectory: string) { 18 | this.templateDirectory = resolve(templateDirectory); 19 | 20 | // Configure Nunjucks environment 21 | this.env = nunjucks.configure(this.templateDirectory, { 22 | autoescape: false, 23 | throwOnUndefined: true, 24 | trimBlocks: true, 25 | lstripBlocks: true, 26 | noCache: true, // Ensure fresh reads in CI environment 27 | }); 28 | 29 | this.setupCustomFilters(); 30 | logger.debug(`Template engine initialized with directory: ${this.templateDirectory}`); 31 | } 32 | 33 | static create(inputs: ActionInputs): TemplateEngine { 34 | return new TemplateEngine(inputs.templateDirectory!); 35 | } 36 | 37 | async renderTemplate(templateName: string, context: TemplateContext): Promise { 38 | try { 39 | // Validate template file exists and is safe 40 | await FileUtils.validateTemplateFile(this.templateDirectory, templateName); 41 | 42 | logger.debug(`Rendering template: ${templateName}`, { contextKeys: Object.keys(context) }); 43 | 44 | // Render template 45 | const rendered = this.env.render(templateName, context); 46 | 47 | logger.info(`Template rendered successfully`, { 48 | templateName, 49 | contentLength: rendered.length, 50 | }); 51 | 52 | return rendered; 53 | } catch (error) { 54 | const message = `${ERROR.TEMPLATE.RENDER_ERROR}: ${error instanceof Error ? error.message : String(error)}`; 55 | logger.error(message, error); 56 | throw new Error(message); 57 | } 58 | } 59 | 60 | private resolveFilePath(filePath: string): string | undefined { 61 | const fs = require('fs'); 62 | // Try using the path as absolute if it's within allowed directories 63 | const absolutePath = resolve(filePath); 64 | if ( 65 | (absolutePath.startsWith(this.templateDirectory) || 66 | absolutePath.startsWith(PATHS.TEMP_DIR)) && 67 | fs.existsSync(absolutePath) 68 | ) { 69 | return absolutePath; 70 | } 71 | 72 | // Try resolving relative to template directory 73 | const templatePath = resolve(this.templateDirectory, filePath); 74 | if (templatePath.startsWith(this.templateDirectory) && fs.existsSync(templatePath)) { 75 | return templatePath; 76 | } 77 | // Try resolving relative to tmp directory 78 | const tmpPath = resolve(PATHS.TEMP_DIR, filePath); 79 | if (tmpPath.startsWith(PATHS.TEMP_DIR) && fs.existsSync(tmpPath)) { 80 | return tmpPath; 81 | } 82 | return undefined; 83 | } 84 | 85 | maybeReadFile(filePath: string): string { 86 | try { 87 | if (!filePath) { 88 | logger.warning('Empty file path provided to maybe_read_file filter'); 89 | return ''; 90 | } 91 | const resolvedPath = this.resolveFilePath(filePath); 92 | if (!resolvedPath) { 93 | logger.warning( 94 | `File path ${filePath} not found in allowed directories (template: ${this.templateDirectory}, tmp: ${PATHS.TEMP_DIR})` 95 | ); 96 | return filePath; // Return original path as fallback 97 | } 98 | const fs = require('fs'); 99 | // Synchronous read for template rendering 100 | const content = fs.readFileSync(resolvedPath, 'utf8'); 101 | logger.debug('File read successfully via filter', { 102 | originalPath: filePath, 103 | resolvedPath: resolvedPath, 104 | contentLength: content.length, 105 | }); 106 | return content; 107 | } catch (_error) { 108 | logger.warning('Failed to read file via filter, returning original path', { 109 | filePath, 110 | }); 111 | return filePath; // Return original path as fallback 112 | } 113 | } 114 | 115 | formatFiles(files: Array<{ filename: string; status: string }>): string { 116 | if (!Array.isArray(files) || files.length === 0) { 117 | return 'No files changed'; 118 | } 119 | return files.map(file => `${file.status.toUpperCase()}: ${file.filename}`).join('\n'); 120 | } 121 | 122 | private setupCustomFilters(): void { 123 | this.env.addFilter('maybe_read_file', this.maybeReadFile.bind(this)); 124 | this.env.addFilter('format_files', this.formatFiles.bind(this)); 125 | logger.debug('Custom filters setup complete'); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/services/github-service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * GitHub API service for PR information extraction 3 | */ 4 | 5 | import { Octokit } from '@octokit/rest'; 6 | import { PullRequestInfo, PullRequestFile, PullRequestDiff } from '../types/github.js'; 7 | import { TEMPLATE_CONFIG, ERROR } from '../config/constants.js'; 8 | import { logger } from '../utils/logger.js'; 9 | 10 | export class GitHubService { 11 | private octokit: Octokit; 12 | private owner: string; 13 | private repo: string; 14 | 15 | constructor(config: { token: string; owner: string; repo: string }) { 16 | this.octokit = new Octokit({ auth: config.token }); 17 | this.owner = config.owner; 18 | this.repo = config.repo; 19 | } 20 | 21 | async getPullRequest(pullNumber: number): Promise { 22 | try { 23 | logger.debug(`Fetching PR ${pullNumber} from ${this.owner}/${this.repo}`); 24 | 25 | const { data } = await this.octokit.rest.pulls.get({ 26 | owner: this.owner, 27 | repo: this.repo, 28 | pull_number: pullNumber, 29 | }); 30 | 31 | return { 32 | number: data.number, 33 | title: data.title, 34 | body: data.body, 35 | state: data.state, 36 | user: { login: data.user?.login || 'unknown' }, 37 | head: { 38 | ref: data.head.ref, 39 | sha: data.head.sha, 40 | repo: { 41 | full_name: data.head.repo.full_name, 42 | name: data.head.repo.name, 43 | owner: { 44 | login: data.head.repo.owner.login, 45 | }, 46 | }, 47 | }, 48 | base: { 49 | ref: data.base.ref, 50 | sha: data.base.sha, 51 | repo: { 52 | full_name: data.base.repo.full_name, 53 | name: data.base.repo.name, 54 | owner: { 55 | login: data.base.repo.owner.login, 56 | }, 57 | }, 58 | }, 59 | }; 60 | } catch (error) { 61 | logger.error(`${ERROR.GITHUB.API_ERROR}: Failed to fetch PR ${pullNumber}`, error); 62 | throw error; 63 | } 64 | } 65 | 66 | async getPullRequestFiles(pullNumber: number): Promise { 67 | try { 68 | logger.debug(`Fetching files for PR ${pullNumber}`); 69 | 70 | const allFiles: PullRequestFile[] = []; 71 | let page = 1; 72 | const perPage = 100; // GitHub's maximum per page 73 | 74 | while (true) { 75 | logger.debug(`Fetching PR files page ${page}`, { 76 | pullNumber, 77 | page, 78 | perPage, 79 | }); 80 | 81 | const { data } = await this.octokit.rest.pulls.listFiles({ 82 | owner: this.owner, 83 | repo: this.repo, 84 | pull_number: pullNumber, 85 | per_page: perPage, 86 | page, 87 | }); 88 | 89 | // Map and add files from this page 90 | const pageFiles = data.map(file => ({ 91 | filename: file.filename, 92 | status: file.status, 93 | additions: file.additions, 94 | deletions: file.deletions, 95 | changes: file.changes, 96 | })); 97 | 98 | allFiles.push(...pageFiles); 99 | 100 | logger.debug(`Fetched ${pageFiles.length} files from page ${page}`, { 101 | pullNumber, 102 | page, 103 | filesOnPage: pageFiles.length, 104 | totalFilesSoFar: allFiles.length, 105 | }); 106 | 107 | // If we got fewer files than the page size, we've reached the end 108 | if (data.length < perPage) { 109 | break; 110 | } 111 | 112 | page++; 113 | } 114 | 115 | logger.info(`Successfully fetched all PR files`, { 116 | pullNumber, 117 | totalFiles: allFiles.length, 118 | totalPages: page, 119 | }); 120 | 121 | return allFiles; 122 | } catch (error) { 123 | logger.error(`${ERROR.GITHUB.API_ERROR}: Failed to fetch PR files`, error); 124 | throw error; 125 | } 126 | } 127 | 128 | async getPullRequestDiff(pullNumber: number): Promise { 129 | try { 130 | logger.debug(`Fetching diff for PR ${pullNumber}`); 131 | 132 | const { data } = await this.octokit.rest.pulls.get({ 133 | owner: this.owner, 134 | repo: this.repo, 135 | pull_number: pullNumber, 136 | mediaType: { format: 'diff' }, 137 | }); 138 | 139 | const content = data as unknown as string; 140 | const size = Buffer.byteLength(content, 'utf8'); 141 | const truncated = size > TEMPLATE_CONFIG.MAX_DIFF_SIZE; 142 | 143 | return { 144 | content: truncated ? content.substring(0, TEMPLATE_CONFIG.MAX_DIFF_SIZE) : content, 145 | size, 146 | truncated, 147 | }; 148 | } catch (error) { 149 | logger.error(`${ERROR.GITHUB.API_ERROR}: Failed to fetch PR diff`, error); 150 | throw error; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/template/extractors/pr-extractor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PR data extractor - responsible for extracting and formatting PR data from GitHub API 3 | */ 4 | 5 | import { join } from 'node:path'; 6 | import { BaseExtractor } from './base-extractor.js'; 7 | import { GitHubService } from '../../services/github-service.js'; 8 | import { FileUtils } from '../../utils/file-utils.js'; 9 | import { logger } from '../../utils/logger.js'; 10 | import { ValidationUtils } from '../../utils/validation.js'; 11 | import { PATHS } from '../../config/constants.js'; 12 | import type { PullRequestFile } from '../../types/github.js'; 13 | import type { PRData } from '../../types/context.js'; 14 | import type { ActionInputs } from '../../types/inputs.js'; 15 | 16 | export class PRExtractor extends BaseExtractor { 17 | constructor(private githubService?: GitHubService) { 18 | super('PR Data'); 19 | } 20 | 21 | /** 22 | * Creates a PRExtractor instance configured for the given inputs 23 | */ 24 | static create(inputs: ActionInputs): PRExtractor { 25 | // If we have complete repo information, create with pre-configured GitHub service 26 | if (inputs.repoName && inputs.githubToken) { 27 | try { 28 | const repoInfo = ValidationUtils.parseRepoName(inputs.repoName); 29 | const githubService = new GitHubService({ 30 | token: inputs.githubToken, 31 | owner: repoInfo.owner, 32 | repo: repoInfo.repo, 33 | }); 34 | logger.debug('PRExtractor created with pre-configured GitHubService', { 35 | owner: repoInfo.owner, 36 | repo: repoInfo.repo, 37 | }); 38 | return new PRExtractor(githubService); 39 | } catch (error) { 40 | logger.debug('Failed to create pre-configured GitHubService, will create lazily', { 41 | error: error instanceof Error ? error.message : 'Unknown error', 42 | }); 43 | // Fall through to create without GitHubService 44 | } 45 | } 46 | 47 | // Create without GitHubService for lazy configuration 48 | logger.debug('PRExtractor created without GitHubService (will be created lazily)'); 49 | return new PRExtractor(); 50 | } 51 | 52 | /** 53 | * Determines if PR extraction should be performed 54 | */ 55 | shouldExtract(inputs: ActionInputs): boolean { 56 | return !!(inputs.pullNumber && inputs.pullNumber > 0 && inputs.repoName && inputs.githubToken); 57 | } 58 | 59 | /** 60 | * Performs the actual PR data extraction 61 | */ 62 | protected async performExtraction(inputs: ActionInputs): Promise { 63 | const pullNumber = inputs.pullNumber!; 64 | 65 | // Get or create GitHubService lazily 66 | const githubService = this.githubService; 67 | if (!githubService) { 68 | throw new Error('GitHubService not provided'); 69 | } 70 | 71 | const prData = await githubService.getPullRequest(pullNumber); 72 | const files = await githubService.getPullRequestFiles(pullNumber); 73 | const diffFilePath = await this.writeDiffFile(pullNumber, githubService); 74 | 75 | const extractedData: PRData = { 76 | number: prData.number, 77 | title: prData.title, 78 | author: prData.user.login, 79 | head: { 80 | ref: prData.head.ref, 81 | sha: prData.head.sha, 82 | repo: { 83 | full_name: prData.head.repo.full_name, 84 | name: prData.head.repo.name, 85 | owner: prData.head.repo.owner.login, 86 | }, 87 | }, 88 | base: { 89 | ref: prData.base.ref, 90 | sha: prData.base.sha, 91 | repo: { 92 | full_name: prData.base.repo.full_name, 93 | name: prData.base.repo.name, 94 | owner: prData.base.repo.owner.login, 95 | }, 96 | }, 97 | body: prData.body || '', 98 | state: prData.state, 99 | changed_files: files.map(file => file.filename).join('\n'), 100 | changed_files_list: files, 101 | diff_file: diffFilePath, 102 | }; 103 | 104 | logger.info('PR data extraction completed', { 105 | pullNumber, 106 | fileCount: files.length, 107 | diffFile: diffFilePath, 108 | }); 109 | 110 | return extractedData; 111 | } 112 | 113 | private async writeDiffFile(pullNumber: number, githubService: GitHubService): Promise { 114 | try { 115 | const diff = await githubService.getPullRequestDiff(pullNumber); 116 | 117 | if (diff.truncated) { 118 | logger.warning('PR diff was truncated due to size', { 119 | originalSize: diff.size, 120 | truncated: diff.truncated, 121 | }); 122 | } 123 | 124 | const diffFileName = PATHS.DIFF_FILE_PATTERN.replace('{pullNumber}', pullNumber.toString()); 125 | const diffFilePath = join(PATHS.TEMP_DIR, diffFileName); 126 | 127 | await FileUtils.ensureDirectoryExists(PATHS.TEMP_DIR); 128 | await FileUtils.writeFile(diffFilePath, diff.content); 129 | 130 | logger.debug('Diff file written', { diffFilePath, size: diff.size }); 131 | return diffFilePath; 132 | } catch (error) { 133 | logger.error('Failed to write diff file', error); 134 | throw error; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | with: 20 | fetch-depth: 0 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - name: Setup Bun 24 | uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 25 | with: 26 | bun-version: latest 27 | 28 | - name: Install dependencies 29 | run: bun install 30 | 31 | - name: Type check 32 | run: bun run typecheck 33 | 34 | - name: Extract version from tag 35 | id: version 36 | run: | 37 | TAG_NAME=${GITHUB_REF#refs/tags/} 38 | VERSION=${TAG_NAME#v} 39 | MAJOR_VERSION=$(echo $VERSION | cut -d. -f1) 40 | echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT 41 | echo "version=$VERSION" >> $GITHUB_OUTPUT 42 | echo "major_version=v$MAJOR_VERSION" >> $GITHUB_OUTPUT 43 | 44 | - name: Verify package.json version matches tag 45 | run: | 46 | PACKAGE_VERSION=$(jq -r '.version' package.json) 47 | if [ "$PACKAGE_VERSION" != "${{ steps.version.outputs.version }}" ]; then 48 | echo "❌ Version mismatch: package.json has $PACKAGE_VERSION but tag is ${{ steps.version.outputs.version }}" 49 | exit 1 50 | fi 51 | echo "✅ Version verified: $PACKAGE_VERSION" 52 | 53 | - name: Update major version tag 54 | run: | 55 | git config --global user.name "github-actions[bot]" 56 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 57 | git tag -f "${{ steps.version.outputs.major_version }}" -m "Release ${{ steps.version.outputs.major_version }} (latest)" 58 | git push origin "${{ steps.version.outputs.major_version }}" --force 59 | 60 | - name: Create GitHub Release 61 | run: | 62 | # Create release body content 63 | RELEASE_BODY=$(cat << 'EOF' 64 | ## Release ${{ steps.version.outputs.tag_name }} 65 | 66 | ### Quick Usage 67 | ```yaml 68 | - uses: augmentcode/augment-agent@${{ steps.version.outputs.tag_name }} 69 | with: 70 | augment_api_token: ${{ secrets.AUGMENT_API_TOKEN }} 71 | augment_api_url: ${{ vars.AUGMENT_API_URL }} 72 | github_token: ${{ secrets.GITHUB_TOKEN }} 73 | instruction_file: /tmp/instruction.txt 74 | ``` 75 | 76 | ### Major Version Tag 77 | You can also use the major version tag for automatic updates: 78 | ```yaml 79 | - uses: augmentcode/augment-agent@${{ steps.version.outputs.major_version }} 80 | ``` 81 | 82 | ### Documentation 83 | - 📖 [Full Documentation](https://github.com/augmentcode/augment-agent#readme) 84 | - 🚀 [Quick Start Guide](https://github.com/augmentcode/augment-agent#quick-start) 85 | - ⚙️ [Configuration Options](https://github.com/augmentcode/augment-agent#inputs) 86 | - 💡 [Example Workflows](https://github.com/augmentcode/augment-agent#example-workflows) 87 | EOF 88 | ) 89 | 90 | # Create the release using GitHub REST API 91 | echo "📡 Creating GitHub release..." 92 | RESPONSE=$(curl -s -w "%{http_code}" -L \ 93 | -X POST \ 94 | -H "Accept: application/vnd.github+json" \ 95 | -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ 96 | -H "X-GitHub-Api-Version: 2022-11-28" \ 97 | https://api.github.com/repos/${{ github.repository }}/releases \ 98 | -d "$(jq -n \ 99 | --arg tag_name "${{ steps.version.outputs.tag_name }}" \ 100 | --arg name "Release ${{ steps.version.outputs.tag_name }}" \ 101 | --arg body "${RELEASE_BODY}" \ 102 | '{ 103 | "tag_name": $tag_name, 104 | "target_commitish": "main", 105 | "name": $name, 106 | "body": $body, 107 | "draft": false, 108 | "prerelease": false 109 | }')") 110 | 111 | # Extract HTTP status code (last 3 characters) 112 | HTTP_CODE="${RESPONSE: -3}" 113 | RESPONSE_BODY="${RESPONSE%???}" 114 | 115 | # Check if the request was successful 116 | if [ "$HTTP_CODE" -eq 201 ]; then 117 | echo "✅ Successfully created release ${{ steps.version.outputs.tag_name }}" 118 | 119 | # Extract and display the release URL if available 120 | RELEASE_URL=$(echo "$RESPONSE_BODY" | jq -r '.html_url // empty') 121 | if [ -n "$RELEASE_URL" ]; then 122 | echo "🔗 Release URL: $RELEASE_URL" 123 | fi 124 | 125 | echo "🎉 Release process completed successfully!" 126 | else 127 | echo "❌ Failed to create release. HTTP status: $HTTP_CODE" 128 | echo "Response: $RESPONSE_BODY" 129 | exit 1 130 | fi 131 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Augment Agent GitHub Action 5 | * Main entry point for the action 6 | */ 7 | 8 | import { spawn, SpawnOptions } from 'child_process'; 9 | import process from 'process'; 10 | import { ValidationUtils } from './utils/validation.js'; 11 | import { TemplateProcessor } from './template/template-processor.js'; 12 | import { logger } from './utils/logger.js'; 13 | import { ActionInputs } from './types/inputs.js'; 14 | 15 | /** 16 | * Execute a shell command and return a promise 17 | */ 18 | function execCommand( 19 | command: string, 20 | args: string[] = [], 21 | options: SpawnOptions = {} 22 | ): Promise { 23 | return new Promise((resolve, reject) => { 24 | // Join command and args into a single shell command for proper quoting 25 | const fullCommand = `${command} ${args 26 | .map(arg => { 27 | // Properly quote arguments that contain spaces or special characters 28 | if (arg.includes(' ') || arg.includes('"') || arg.includes("'")) { 29 | return `"${arg.replace(/"/g, '\\"')}"`; 30 | } 31 | return arg; 32 | }) 33 | .join(' ')}`; 34 | 35 | const child = spawn(fullCommand, [], { 36 | stdio: 'inherit', 37 | shell: true, 38 | ...options, 39 | }); 40 | 41 | child.on('close', code => { 42 | if (code === 0) { 43 | resolve(code); 44 | } else { 45 | reject(new Error(`Command failed with exit code ${code}`)); 46 | } 47 | }); 48 | 49 | child.on('error', error => { 50 | reject(error); 51 | }); 52 | }); 53 | } 54 | 55 | /** 56 | * Set up environment variables for the augment script 57 | */ 58 | function setupEnvironment(inputs: ActionInputs): void { 59 | // Set authentication environment variables 60 | if (inputs.augmentSessionAuth) { 61 | // Use session authentication 62 | process.env.AUGMENT_SESSION_AUTH = inputs.augmentSessionAuth; 63 | } else { 64 | // Use token + URL authentication 65 | process.env.AUGMENT_API_TOKEN = inputs.augmentApiToken; 66 | process.env.AUGMENT_API_URL = inputs.augmentApiUrl; 67 | } 68 | 69 | // Set GitHub token if provided 70 | if (inputs.githubToken) { 71 | process.env.GITHUB_API_TOKEN = inputs.githubToken; 72 | } 73 | } 74 | 75 | /** 76 | * Process templates and generate instruction file 77 | */ 78 | async function processTemplate(inputs: ActionInputs): Promise { 79 | logger.info('Preparing template', { 80 | templateDirectory: inputs.templateDirectory, 81 | templateName: inputs.templateName, 82 | }); 83 | 84 | // Create and run template processor 85 | const processor = TemplateProcessor.create(inputs); 86 | const instructionFilePath = await processor.processTemplate(inputs); 87 | 88 | logger.info('Template processing completed', { 89 | instructionFile: instructionFilePath, 90 | }); 91 | 92 | return instructionFilePath; 93 | } 94 | 95 | /** 96 | * Run the augment script with appropriate arguments 97 | */ 98 | async function runAugmentScript(inputs: ActionInputs): Promise { 99 | let instruction_value: string; 100 | let is_file: boolean; 101 | if (inputs.instruction) { 102 | instruction_value = inputs.instruction; 103 | is_file = false; 104 | logger.debug('Using direct instruction', { instruction: inputs.instruction }); 105 | } else if (inputs.instructionFile) { 106 | instruction_value = inputs.instructionFile; 107 | is_file = true; 108 | logger.debug('Using instruction file', { instructionFile: inputs.instructionFile }); 109 | } else { 110 | instruction_value = await processTemplate(inputs); 111 | is_file = true; 112 | logger.debug('Using template-generated instruction file', { 113 | instructionFile: instruction_value, 114 | }); 115 | } 116 | const args = ['--print']; 117 | if (inputs.model && inputs.model.trim().length > 0) { 118 | args.push('--model', inputs.model.trim()); 119 | } 120 | if (is_file) { 121 | logger.info(`📄 Using instruction file: ${instruction_value}`); 122 | args.push('--instruction-file', instruction_value); 123 | } else { 124 | logger.info('📝 Using direct instruction'); 125 | args.push('--instruction', instruction_value); 126 | } 127 | 128 | const uniqueRules = Array.from(new Set(inputs.rules ?? [])).filter(rule => rule.length > 0); 129 | if (uniqueRules.length > 0) { 130 | logger.info(`🔧 Applying ${uniqueRules.length} rule file(s)`); 131 | for (const rulePath of uniqueRules) { 132 | logger.info(` - ${rulePath}`); 133 | args.push('--rules', rulePath); 134 | } 135 | } 136 | 137 | const uniqueMcpConfigs = Array.from(new Set(inputs.mcpConfigs ?? [])).filter( 138 | config => config.length > 0 139 | ); 140 | if (uniqueMcpConfigs.length > 0) { 141 | logger.info(`🧩 Applying ${uniqueMcpConfigs.length} MCP config file(s)`); 142 | for (const configPath of uniqueMcpConfigs) { 143 | logger.info(` - ${configPath}`); 144 | args.push('--mcp-config', configPath); 145 | } 146 | } 147 | 148 | await execCommand('auggie', args); 149 | logger.info('✅ Augment Agent completed successfully'); 150 | } 151 | 152 | /** 153 | * Main function 154 | */ 155 | async function main(): Promise { 156 | try { 157 | logger.info('🔍 Validating inputs...'); 158 | const inputs = ValidationUtils.validateInputs(); 159 | 160 | logger.info('⚙️ Setting up environment...'); 161 | setupEnvironment(inputs); 162 | 163 | logger.info('🚀 Starting Augment Agent...'); 164 | await runAugmentScript(inputs); 165 | } catch (error) { 166 | const errorMessage = error instanceof Error ? error.message : String(error); 167 | logger.setFailed(errorMessage); 168 | } 169 | } 170 | 171 | // Run the action only if this file is executed directly 172 | if (import.meta.url === `file://${process.argv[1]}`) { 173 | main().catch(error => { 174 | const errorMessage = error instanceof Error ? error.message : String(error); 175 | logger.setFailed(`Unexpected error: ${errorMessage}`); 176 | }); 177 | } 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auggie Agent GitHub Action 2 | 3 | AI-powered code assistance for GitHub pull requests using Auggie. 4 | 5 | ## Quick Start 6 | 7 | ### 1. Get Your Augment API Credentials 8 | 9 | First, you'll need to obtain your Augment Authentication information from your local Auggie session: 10 | 11 | Example session JSON: 12 | 13 | ```json 14 | { 15 | "accessToken": "your-api-token-here", 16 | "tenantURL": "https://your-tenant.api.augmentcode.com" 17 | } 18 | ``` 19 | 20 | There are 2 ways to get the credentials: 21 | 22 | - Run `auggie tokens print` 23 | - Copy the JSON after `TOKEN=` 24 | - Copy the credentials stored in your Augment cache directory, defaulting to `~/.augment/session.json` 25 | 26 | > **⚠️ Security Warning**: These tokens are OAuth tokens tied to your personal Augment account and provide access to your Augment services. They are not tied to a team or enterprise. Treat them as sensitive credentials: 27 | > 28 | > - Never commit them to version control 29 | > - Only store them in secure locations (like GitHub secrets) 30 | > - Don't share them in plain text or expose them in logs 31 | > - If a token is compromised, immediately revoke it using `auggie tokens revoke` 32 | 33 | ### 2. Set Up the GitHub Repository Secret 34 | 35 | You need to add your Augment credentials to your GitHub repository: 36 | 37 | #### Adding Secret 38 | 39 | 1. **Navigate to your repository** on GitHub 40 | 2. **Go to Settings** → **Secrets and variables** → **Actions** 41 | 3. **Add the following**: 42 | - **Secret**: Click "New repository secret" 43 | - Name: `AUGMENT_SESSION_AUTH` 44 | - Value: The json value from step 1 45 | 46 | > **Need more help?** For detailed instructions on managing GitHub secrets, see GitHub's official documentation: 47 | > 48 | > - [Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) 49 | 50 | ### 3. Create Your Workflow File 51 | 52 | Add a new workflow file to your repository's `.github/workflows/` directory and merge it. 53 | 54 | #### Example Workflows 55 | 56 | For complete workflow examples, see the [`example-workflows/`](./example-workflows/) directory which contains: 57 | 58 | - **PR Description Generation** - Automatically generate comprehensive PR descriptions 59 | - **Code Review** - Perform automated code quality and security reviews 60 | - **Issue Triage** - Automatically analyze and triage GitHub issues with priority and categorization recommendations 61 | - **Template-Based Review** - Demonstrates the template system for dynamic, context-aware instructions 62 | 63 | Each example includes a complete workflow file that you can copy to your `.github/workflows/` directory and customize for your needs. 64 | 65 | ## Advanced 66 | 67 | ### Inputs 68 | 69 | | Input | Description | Required | Example | 70 | | ---------------------- | --------------------------------------------------------------------------------------- | -------- | ------------------------------------------- | 71 | | `augment_session_auth` | Augment session authentication JSON (store as secret) | No\*\* | `${{ secrets.AUGMENT_SESSION_AUTH }}` | 72 | | `augment_api_token` | API token for Augment services (store as secret) | No\*\* | `${{ secrets.AUGMENT_API_TOKEN }}` | 73 | | `augment_api_url` | Augment API endpoint URL (store as variable) | No\*\* | `${{ vars.AUGMENT_API_URL }}` | 74 | | `github_token` | GitHub token with `repo` and `user:email` scopes. | No | `${{ secrets.GITHUB_TOKEN }}` | 75 | | `instruction` | Direct instruction text for simple commands | No\* | `"Generate PR description"` | 76 | | `instruction_file` | Path to file with detailed instructions | No\* | `/tmp/instruction.txt` | 77 | | `template_directory` | Path to template directory for dynamic instructions | No\* | `.github/templates` | 78 | | `template_name` | Template file name (default: prompt.njk) | No | `pr-review.njk` | 79 | | `pull_number` | PR number for template context extraction | No | `${{ github.event.pull_request.number }}` | 80 | | `repo_name` | Repository name for template context | No | `${{ github.repository }}` | 81 | | `custom_context` | Additional JSON context for templates | No | `'{"priority": "high"}'` | 82 | | `model` | Model to use; passed through to auggie as --model | No | e.g. `sonnet4`, from `auggie --list-models` | 83 | | `rules` | JSON array of rules file paths (each forwarded as individual `--rules` flags) | No | `'[".github/augment/rules.md"]'` | 84 | | `mcp_configs` | JSON array of MCP config file paths (each forwarded as individual `--mcp-config` flags) | No | `'[".augment/mcp/config.json"]'` | 85 | 86 | \*Either `instruction`, `instruction_file`, or `template_directory` must be provided. 87 | 88 | \*\*Either `augment_session_auth` OR both `augment_api_token` and `augment_api_url` must be provided for authentication. 89 | 90 | ### Template System 91 | 92 | For advanced use cases, the Auggie Agent supports a template system that automatically extracts context from GitHub pull requests and allows you to create dynamic, reusable instruction templates. Templates are ideal when you need instructions that adapt based on PR content, file changes, or custom data. 93 | 94 | See [TEMPLATE.md](./TEMPLATE.md) for complete documentation on creating and using templates. 95 | -------------------------------------------------------------------------------- /TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Template System Documentation 2 | 3 | The Auggie Agent supports a powerful template system that allows you to create reusable, dynamic instruction templates using the Nunjucks templating engine. Templates enable you to automatically extract and inject contextual data from GitHub pull requests and custom sources into your AI instructions. 4 | 5 | ## Why Use Templates? 6 | 7 | Templates are ideal when you need: 8 | 9 | - **Dynamic content**: Instructions that change based on PR data, file changes, or custom context 10 | - **Reusable workflows**: Standardized instruction patterns across multiple repositories 11 | - **Rich context**: Automatic extraction of PR details, file lists, and diffs 12 | - **Complex logic**: Conditional content, loops, and data formatting 13 | 14 | ## Basic Usage 15 | 16 | ```yaml 17 | - name: Run Auggie Agent with Template 18 | uses: augmentcode/augment-agent@v0 19 | with: 20 | augment_session_auth: ${{ secrets.AUGMENT_SESSION_AUTH }} 21 | github_token: ${{ secrets.GITHUB_TOKEN }} 22 | template_directory: '.github/templates' 23 | template_name: 'pr-review.njk' 24 | pull_number: ${{ github.event.pull_request.number }} 25 | repo_name: ${{ github.repository }} 26 | ``` 27 | 28 | ## Template Inputs 29 | 30 | | Input | Description | Required | Example | 31 | | -------------------- | -------------------------------------- | -------------------------- | ----------------------------------------- | 32 | | `template_directory` | Path to directory containing templates | Yes | `.github/templates` | 33 | | `template_name` | Template file name | No (default: `prompt.njk`) | `code-review.njk` | 34 | | `pull_number` | PR number for context extraction | No\* | `${{ github.event.pull_request.number }}` | 35 | | `repo_name` | Repository in `owner/repo` format | No\* | `${{ github.repository }}` | 36 | | `custom_context` | Additional JSON context data | No | `'{"priority": "high"}'` | 37 | 38 | \*Required together when using PR context extraction 39 | 40 | ## Available Context Data 41 | 42 | ### PR Context (`pr`) 43 | 44 | When `pull_number` and `repo_name` are provided, the following PR data is automatically extracted: 45 | 46 | ```nunjucks 47 | {# Basic PR information #} 48 | {{ pr.number }} {# PR number #} 49 | {{ pr.title }} {# PR title #} 50 | {{ pr.body }} {# PR description/body #} 51 | {{ pr.author }} {# PR author username #} 52 | {{ pr.state }} {# PR state (open, closed, etc.) #} 53 | 54 | {# Branch information #} 55 | {{ pr.head.ref }} {# Source branch name #} 56 | {{ pr.head.sha }} {# Source commit SHA #} 57 | {{ pr.head.repo.name }} {# Source repository name #} 58 | {{ pr.head.repo.owner }} {# Source repository owner #} 59 | 60 | {{ pr.base.ref }} {# Target branch name #} 61 | {{ pr.base.sha }} {# Target commit SHA #} 62 | {{ pr.base.repo.name }} {# Target repository name #} 63 | {{ pr.base.repo.owner }} {# Target repository owner #} 64 | 65 | {# File changes #} 66 | {{ pr.changed_files }} {# Newline-separated list of changed files #} 67 | {{ pr.diff_file }} {# Path to generated diff file #} 68 | ``` 69 | 70 | ### File List (`pr.changed_files_list`) 71 | 72 | Array of changed files with detailed information: 73 | 74 | ```nunjucks 75 | {% for file in pr.changed_files_list %} 76 | File: {{ file.filename }} 77 | Status: {{ file.status }} {# added, modified, removed, renamed #} 78 | Additions: {{ file.additions }} 79 | Deletions: {{ file.deletions }} 80 | Changes: {{ file.changes }} 81 | {% endfor %} 82 | ``` 83 | 84 | ### Custom Context (`custom`) 85 | 86 | Any additional data provided via the `custom_context` input: 87 | 88 | ```nunjucks 89 | {# If custom_context: '{"priority": "high", "team": "backend"}' #} 90 | {{ custom.priority }} {# "high" #} 91 | {{ custom.team }} {# "backend" #} 92 | ``` 93 | 94 | ## Template Filters 95 | 96 | ### `maybe_read_file` 97 | 98 | Safely reads file content from allowed directories: 99 | 100 | ```nunjucks 101 | {# Read the diff file #} 102 | {{ pr.diff_file | maybe_read_file }} 103 | 104 | {# Read a custom file from template directory #} 105 | {{ "instructions/common.txt" | maybe_read_file }} 106 | ``` 107 | 108 | ### `format_files` 109 | 110 | Formats file arrays for display: 111 | 112 | ```nunjucks 113 | {{ pr.changed_files_list | format_files }} 114 | {# Output: MODIFIED: src/file1.ts 115 | ADDED: src/file2.ts 116 | DELETED: old/file3.ts #} 117 | ``` 118 | 119 | ## Example Templates 120 | 121 | ### Basic PR Review Template 122 | 123 | ```nunjucks 124 | {# .github/templates/pr-review.njk #} 125 | Please review pull request #{{ pr.number }}: "{{ pr.title }}" 126 | 127 | **Author:** {{ pr.author }} 128 | **Branch:** {{ pr.head.ref }} → {{ pr.base.ref }} 129 | 130 | **Changed Files:** 131 | {{ pr.changed_files_list | format_files }} 132 | 133 | **Code Changes:** 134 | {{ pr.diff_file | maybe_read_file }} 135 | 136 | Please provide a thorough code review focusing on: 137 | - Code quality and best practices 138 | - Security considerations 139 | - Performance implications 140 | - Testing coverage 141 | 142 | {% if custom.priority == "high" %} 143 | ⚠️ This is a high-priority PR requiring urgent review. 144 | {% endif %} 145 | ``` 146 | 147 | ### Conditional Logic Template 148 | 149 | ```nunjucks 150 | {# .github/templates/smart-review.njk #} 151 | Reviewing PR #{{ pr.number }}: {{ pr.title }} 152 | 153 | {% if pr.changed_files_list | length > 10 %} 154 | This is a large PR with {{ pr.changed_files_list | length }} files changed. 155 | Please pay special attention to: 156 | {% for file in pr.changed_files_list[:5] %} 157 | - {{ file.filename }} ({{ file.changes }} changes) 158 | {% endfor %} 159 | {% else %} 160 | This is a focused PR with the following changes: 161 | {% for file in pr.changed_files_list %} 162 | - {{ file.filename }}: {{ file.status }} ({{ file.changes }} changes) 163 | {% endfor %} 164 | {% endif %} 165 | 166 | {% set has_tests = pr.changed_files | includes("test") or pr.changed_files | includes("spec") %} 167 | {% if not has_tests %} 168 | ⚠️ No test files detected. Please verify test coverage. 169 | {% endif %} 170 | 171 | {{ pr.diff_file | maybe_read_file }} 172 | ``` 173 | 174 | ## File Organization 175 | 176 | Organize your templates in a dedicated directory: 177 | 178 | ``` 179 | .github/ 180 | templates/ 181 | prompt.njk # Default template 182 | pr-review.njk # Code review template 183 | security-check.njk # Security-focused review 184 | instructions/ 185 | common.txt # Shared instruction snippets 186 | ``` 187 | 188 | ## Security Notes 189 | 190 | - Templates can only read files from the template directory and `/tmp` 191 | - File paths are validated to prevent directory traversal 192 | - Template size is limited to 128KB 193 | - Diff content is truncated at 128KB for large PRs 194 | 195 | ## Troubleshooting 196 | 197 | **Template not found**: Ensure the template file exists in the specified directory and the path is correct. 198 | 199 | **Context missing**: Verify that `pull_number` and `repo_name` are provided when using PR context. 200 | 201 | **Invalid JSON**: Check that `custom_context` contains valid JSON if provided. 202 | 203 | **File read errors**: Ensure files referenced in templates exist and are within allowed directories. 204 | -------------------------------------------------------------------------------- /src/utils/validation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Input validation utilities 3 | */ 4 | 5 | import { z } from 'zod'; 6 | import type { ActionInputs, RepoInfo } from '../types/inputs.js'; 7 | import { logger } from './logger.js'; 8 | import { ERROR, INPUT_FIELD_MAP } from '../config/constants.js'; 9 | 10 | const createJsonStringArraySchema = (invalidTypeMessage: string, emptyEntryMessage: string) => 11 | z.preprocess( 12 | value => { 13 | if (value === undefined || value === null) { 14 | return undefined; 15 | } 16 | if (typeof value === 'string') { 17 | const trimmed = value.trim(); 18 | if (trimmed.length === 0) { 19 | return undefined; 20 | } 21 | try { 22 | return JSON.parse(trimmed); 23 | } catch { 24 | return value; 25 | } 26 | } 27 | return value; 28 | }, 29 | z 30 | .array( 31 | z 32 | .string() 33 | .transform(val => val.trim()) 34 | .refine(val => val.length > 0, { message: emptyEntryMessage }), 35 | { invalid_type_error: invalidTypeMessage } 36 | ) 37 | .optional() 38 | ); 39 | 40 | /** 41 | * Zod schema for action inputs validation 42 | */ 43 | const ActionInputsSchema = z 44 | .object({ 45 | augmentSessionAuth: z.string().optional(), 46 | augmentApiToken: z.string().optional(), 47 | augmentApiUrl: z.string().optional(), 48 | githubToken: z.string().optional(), 49 | instruction: z.string().optional(), 50 | instructionFile: z.string().optional(), 51 | model: z.string().optional(), 52 | templateDirectory: z.string().optional(), 53 | templateName: z.string().default('prompt.njk'), 54 | customContext: z.string().optional(), 55 | pullNumber: z.number().int().positive('Pull number must be a positive integer').optional(), 56 | repoName: z 57 | .string() 58 | .regex(/^[^\/]+\/[^\/]+$/, ERROR.INPUT.REPO_FORMAT) 59 | .optional(), 60 | rules: createJsonStringArraySchema( 61 | 'rules must be a JSON array of strings', 62 | 'Rule file paths cannot be empty' 63 | ), 64 | mcpConfigs: createJsonStringArraySchema( 65 | 'mcp_configs must be a JSON array of strings', 66 | 'MCP config file paths cannot be empty' 67 | ), 68 | }) 69 | .refine( 70 | data => { 71 | const hasInstruction = data.instruction || data.instructionFile; 72 | const hasTemplate = data.templateDirectory; 73 | return hasInstruction || hasTemplate; 74 | }, 75 | { 76 | message: ERROR.INPUT.MISSING_INSTRUCTION_OR_TEMPLATE, 77 | path: ['instruction', 'instructionFile', 'templateDirectory'], 78 | } 79 | ) 80 | .refine( 81 | data => { 82 | const hasInstruction = data.instruction || data.instructionFile; 83 | const hasTemplate = data.templateDirectory; 84 | return !(hasInstruction && hasTemplate); 85 | }, 86 | { 87 | message: ERROR.INPUT.CONFLICTING_INSTRUCTION_TEMPLATE, 88 | path: ['instruction', 'instructionFile', 'templateDirectory'], 89 | } 90 | ) 91 | .refine(data => !(data.instruction && data.instructionFile), { 92 | message: ERROR.INPUT.CONFLICTING_INSTRUCTION_INPUTS, 93 | path: ['instruction', 'instructionFile'], 94 | }) 95 | .refine( 96 | data => { 97 | const hasPullNumber = data.pullNumber !== undefined; 98 | const hasRepoName = data.repoName !== undefined; 99 | return hasPullNumber === hasRepoName; 100 | }, 101 | { 102 | message: ERROR.INPUT.MISMATCHED_PR_FIELDS, 103 | path: ['pullNumber', 'repoName'], 104 | } 105 | ) 106 | .refine( 107 | data => { 108 | if (!data.customContext) return true; 109 | try { 110 | JSON.parse(data.customContext); 111 | return true; 112 | } catch { 113 | return false; 114 | } 115 | }, 116 | { 117 | message: ERROR.INPUT.INVALID_CONTEXT_JSON, 118 | path: ['customContext'], 119 | } 120 | ) 121 | .refine( 122 | data => { 123 | const hasSessionAuth = data.augmentSessionAuth; 124 | const hasTokenAuth = data.augmentApiToken && data.augmentApiUrl; 125 | return hasSessionAuth || hasTokenAuth; 126 | }, 127 | { 128 | message: 129 | 'Either augment_session_auth or both augment_api_token and augment_api_url must be provided', 130 | path: ['augmentSessionAuth', 'augmentApiToken', 'augmentApiUrl'], 131 | } 132 | ) 133 | .refine( 134 | data => { 135 | const hasSessionAuth = data.augmentSessionAuth; 136 | const hasTokenAuth = data.augmentApiToken || data.augmentApiUrl; 137 | return !(hasSessionAuth && hasTokenAuth); 138 | }, 139 | { 140 | message: 141 | 'Cannot use both augment_session_auth and augment_api_token/augment_api_url simultaneously', 142 | path: ['augmentSessionAuth', 'augmentApiToken', 'augmentApiUrl'], 143 | } 144 | ) 145 | .refine( 146 | data => { 147 | if (!data.augmentSessionAuth) return true; 148 | try { 149 | JSON.parse(data.augmentSessionAuth); 150 | return true; 151 | } catch { 152 | return false; 153 | } 154 | }, 155 | { 156 | message: 'augment_session_auth must be valid JSON', 157 | path: ['augmentSessionAuth'], 158 | } 159 | ) 160 | .refine( 161 | data => { 162 | if (!data.augmentApiUrl) return true; 163 | try { 164 | new URL(data.augmentApiUrl); 165 | return true; 166 | } catch { 167 | return false; 168 | } 169 | }, 170 | { 171 | message: 'Augment API URL must be a valid URL', 172 | path: ['augmentApiUrl'], 173 | } 174 | ); 175 | 176 | /** 177 | * Validation utilities 178 | */ 179 | export class ValidationUtils { 180 | /** 181 | * Validate action inputs from environment variables 182 | */ 183 | static validateInputs(): ActionInputs { 184 | try { 185 | logger.debug('Reading action inputs from environment variables'); 186 | 187 | // Build inputs object from field map 188 | const inputs = Object.fromEntries( 189 | Object.entries(INPUT_FIELD_MAP) 190 | .map(([key, fieldDef]) => { 191 | const value = process.env[fieldDef.envVar]; 192 | 193 | // Skip optional fields if no value set 194 | if (!fieldDef.required && !value) { 195 | return null; 196 | } 197 | 198 | // Apply transformation if defined 199 | const transformedValue = 200 | fieldDef.transform && value ? fieldDef.transform(value) : value; 201 | 202 | return [key, transformedValue]; 203 | }) 204 | .filter((entry): entry is [string, any] => entry !== null) 205 | ); 206 | 207 | logger.debug('Validating action inputs'); 208 | const validated = ActionInputsSchema.parse(inputs) as ActionInputs; 209 | logger.debug('Action inputs validated successfully'); 210 | return validated; 211 | } catch (error) { 212 | if (error instanceof z.ZodError) { 213 | const errorMessages = error.errors.map(err => `${err.path.join('.')}: ${err.message}`); 214 | const message = `${ERROR.INPUT.INVALID}: ${errorMessages.join(', ')}`; 215 | logger.error(message, error); 216 | throw new Error(message); 217 | } 218 | logger.error('Unexpected validation error', error); 219 | throw error; 220 | } 221 | } 222 | 223 | /** 224 | * Parse repository name into owner and repo 225 | */ 226 | static parseRepoName(repoName: string): RepoInfo { 227 | logger.debug('Parsing repository name', { repoName }); 228 | 229 | const parts = repoName.split('/'); 230 | if (parts.length !== 2) { 231 | const message = `${ERROR.INPUT.REPO_FORMAT}: ${repoName}`; 232 | logger.error(message); 233 | throw new Error(message); 234 | } 235 | 236 | const [owner, repo] = parts; 237 | if (!owner || !repo) { 238 | const message = `${ERROR.INPUT.REPO_FORMAT}: ${repoName}. Owner and repo cannot be empty`; 239 | logger.error(message); 240 | throw new Error(message); 241 | } 242 | 243 | const result = { owner, repo }; 244 | logger.debug('Repository name parsed successfully', result); 245 | return result; 246 | } 247 | } 248 | --------------------------------------------------------------------------------