diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..5205f8d --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,75 @@ +name: PR + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + branches: + - main + +permissions: + contents: read + pull-requests: write + +jobs: + title: + runs-on: ubuntu-latest + steps: + - name: Validate PR title follows Conventional Commits + env: + TITLE: ${{ github.event.pull_request.title }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if echo "$TITLE" | grep -qE "^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?(!)?: .+"; then + echo "PR title is valid: $TITLE" + exit 0 + fi + + BODY=$(cat <<'COMMENT' + ### ⚠️ Invalid PR Title + + PR title must follow the **Conventional Commits** format since we use squash merge: + + ``` + [optional scope][!]: + ``` + + **Allowed types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `build`, `ci`, `perf`, `revert` + + **Examples:** + - `feat: add new feature` + - `fix(api): resolve null pointer` + - `feat!: breaking change` + - `chore(deps): update dependencies` + COMMENT + ) + + # Post comment on PR + gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \ + -X POST -f body="$BODY" + + echo "::error::PR title must follow Conventional Commits format" + exit 1 + + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: AxeForging/reviewforge@main + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AI_PROVIDER: gemini + AI_MODEL: gemini-2.5-flash + AI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + SHOW_TOKEN_USAGE: true + INCREMENTAL: false + REVIEW_RULES: concise + + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: AxeForging/structlint@main + with: + config: .structlint.yaml + comment-on-pr: "true" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1181c08..8098fa5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,8 +4,9 @@ on: workflow_dispatch: inputs: tag: - description: 'Tag to release (e.g. v1.0.0)' - required: true + description: 'Release tag (leave empty for auto-bump from conventional commits)' + required: false + type: string permissions: contents: write @@ -14,39 +15,52 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v4 + - name: Setup Go + uses: actions/setup-go@v5 with: go-version: '1.24' - check-latest: true + cache: true - name: Run tests - run: go test ./... + run: go test ./... -v - - name: Build binaries - run: VERSION=${{ github.event.inputs.tag }} make build + - name: Determine version + id: version + uses: AxeForging/releaseforge@main + with: + command: bump - - name: Create archives + - name: Set tag + id: tag run: | - cd dist - for f in *; do - if [[ "$f" == *.exe ]]; then - zip "${f%.exe}.zip" "$f" - else - tar czf "$f.tar.gz" "$f" - fi - done - cd .. - - - name: Create Release - uses: softprops/action-gh-release@v1 + if [ -n "${{ inputs.tag }}" ]; then + echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" + else + echo "tag=${{ steps.version.outputs.next-version }}" >> "$GITHUB_OUTPUT" + fi + + - name: Generate release notes + id: notes + uses: AxeForging/releaseforge@main + with: + command: generate + api-key: ${{ secrets.GEMINI_API_KEY }} + + - name: Create and push tag + run: | + echo "Releasing ${{ steps.tag.outputs.tag }}" + git tag ${{ steps.tag.outputs.tag }} + git push origin ${{ steps.tag.outputs.tag }} + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 with: - tag_name: ${{ github.event.inputs.tag }} - files: | - dist/*.tar.gz - dist/*.zip - draft: false - prerelease: false - generate_release_notes: true + version: latest + args: release --clean --release-notes ${{ steps.notes.outputs.release-notes }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml deleted file mode 100644 index 8ef5e34..0000000 --- a/.github/workflows/review.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Self-Review -on: - pull_request: - types: [opened, synchronize, reopened] - branches: - - main - -permissions: - contents: read - pull-requests: write - -jobs: - review: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: AxeForging/reviewforge@main - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AI_PROVIDER: gemini - AI_MODEL: gemini-2.5-flash - AI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - SHOW_TOKEN_USAGE: true - INCREMENTAL: false - PERSONA: "" - REVIEW_RULES: concise diff --git a/.github/workflows/reviewforge.yml b/.github/workflows/reviewforge.yml new file mode 100644 index 0000000..86b00be --- /dev/null +++ b/.github/workflows/reviewforge.yml @@ -0,0 +1,66 @@ +name: ReviewForge + +on: + workflow_call: + inputs: + ai-provider: + description: "AI provider: openai, anthropic, gemini" + type: string + default: "gemini" + ai-model: + description: "AI model to use" + type: string + default: "gemini-2.5-flash" + incremental: + description: "Only review new changes since last bot review" + type: boolean + default: false + show-token-usage: + description: "Append AI token usage metrics to the review summary" + type: boolean + default: false + persona: + description: "Reviewer persona: bob, robert, maya, eli" + type: string + default: "" + review-rules: + description: "Built-in comment rules preset: concise, thorough" + type: string + default: "concise" + max-comments: + description: "Maximum number of line comments (0 = unlimited)" + type: string + default: "5" + exclude-patterns: + description: "Comma-separated glob patterns to exclude" + type: string + default: "**/*.lock,**/*.json,**/*.md" + secrets: + ai_api_key: + description: "API key for the chosen AI provider" + required: true + github_token: + description: "GitHub token for API access" + required: false + +permissions: + contents: read + pull-requests: write + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: AxeForging/reviewforge@main + with: + GITHUB_TOKEN: ${{ secrets.github_token || github.token }} + AI_PROVIDER: ${{ inputs.ai-provider }} + AI_MODEL: ${{ inputs.ai-model }} + AI_API_KEY: ${{ secrets.ai_api_key }} + INCREMENTAL: ${{ inputs.incremental }} + SHOW_TOKEN_USAGE: ${{ inputs.show-token-usage }} + PERSONA: ${{ inputs.persona }} + REVIEW_RULES: ${{ inputs.review-rules }} + MAX_COMMENTS: ${{ inputs.max-comments }} + EXCLUDE_PATTERNS: ${{ inputs.exclude-patterns }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7c7bc62..2c2a475 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,20 +2,22 @@ name: Test on: push: - branches: [ "**" ] pull_request: + branches: + - main jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v4 + - name: Setup Go + uses: actions/setup-go@v5 with: go-version: '1.24' - check-latest: true + cache: true - name: Download dependencies run: go mod download diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..852e799 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,81 @@ +# GoReleaser configuration for ReviewForge +version: 2 + +before: + hooks: + - go mod tidy + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + - 386 + - arm + ldflags: + - -s -w + - -X main.Version={{.Version}} + - -X main.BuildTime={{.Date}} + - -X main.GitCommit={{.FullCommit}} + ignore: + - goos: windows + goarch: arm + +archives: + - formats: + - tar.gz + name_template: "reviewforge-{{ .Os }}-{{ .Arch }}" + files: + - README.md + - LICENSE + format_overrides: + - goos: windows + formats: + - zip + +checksum: + name_template: checksums.txt + +snapshot: + version_template: "{{ incpatch .Version }}-next" + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +release: + github: + owner: AxeForging + name: reviewforge + draft: false + prerelease: auto + name_template: "Release {{ .Tag }}" + footer: | + ## Installation + + ### Go Install + ```bash + go install github.com/AxeForging/reviewforge@{{ .Tag }} + ``` + + ### Linux/macOS (AMD64) + ```bash + curl -L https://github.com/AxeForging/reviewforge/releases/download/{{ .Tag }}/reviewforge-linux-amd64.tar.gz | tar xz + chmod +x reviewforge-linux-amd64 + sudo mv reviewforge-linux-amd64 /usr/local/bin/reviewforge + ``` + + ### Linux/macOS (ARM64) + ```bash + curl -L https://github.com/AxeForging/reviewforge/releases/download/{{ .Tag }}/reviewforge-linux-arm64.tar.gz | tar xz + chmod +x reviewforge-linux-arm64 + sudo mv reviewforge-linux-arm64 /usr/local/bin/reviewforge + ``` diff --git a/.structlint.yaml b/.structlint.yaml new file mode 100644 index 0000000..dd71efb --- /dev/null +++ b/.structlint.yaml @@ -0,0 +1,84 @@ +# structlint configuration +# Validates project directory structure and file naming + +dir_structure: + allowedPaths: + - "." + - "actions/**" + - "services/**" + - "helpers/**" + - "domain/**" + - "integration/**" + - "doc/**" + - "dist/**" + - ".claude/**" + - ".github/**" + disallowedPaths: + - "vendor/**" + - "node_modules/**" + - "tmp/**" + - "temp/**" + - ".git/**" + - "*.log" + requiredPaths: + - "actions" + - "services" + - "domain" + +file_naming_pattern: + allowed: + - "*.go" + - "*.mod" + - "*.sum" + - "*.yaml" + - "*.yml" + - "*.json" + - "*.toml" + - "*.md" + - "*.txt" + - "*.png" + - "*.jpg" + - "*.svg" + - "README*" + - "LICENSE*" + - "CHANGELOG*" + - "Makefile" + - "Dockerfile*" + - "*.sh" + - ".gitignore" + - ".editorconfig" + - ".golangci.yml" + - ".goreleaser.yml" + - ".github/**" + - "go.work" + - "go.work.sum" + disallowed: + - "*.env*" + - ".env*" + - "*.key" + - "*.pem" + - "*.log" + - "*.tmp" + - "*.temp" + - "*~" + - "*.swp" + - "*.bak" + - ".DS_Store" + - "Thumbs.db" + required: + - "go.mod" + - "README.md" + - ".gitignore" + - "*.go" + +ignore: + - ".git" + - "vendor" + - "node_modules" + - "bin" + - "dist" + - ".idea" + - ".vscode" + - ".DS_Store" + - "*.log" + - "*.tmp" diff --git a/action.yml b/action.yml index 599097f..48edc24 100644 --- a/action.yml +++ b/action.yml @@ -86,7 +86,10 @@ runs: VERSION="${{ github.action_ref }}" if [ -z "$VERSION" ] || [ "$VERSION" == "main" ]; then - VERSION="v1.5.0" + # Fetch latest release tag + VERSION=$(curl -sS -o /dev/null -w '%{redirect_url}' \ + "https://github.com/AxeForging/reviewforge/releases/latest" | grep -oP '[^/]+$') + echo "Resolved latest version: ${VERSION}" fi # Download and extract the specific binary (Public asset, no token needed) diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..d59b16f --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,29 @@ +# Lefthook configuration +# Install: go install github.com/evilmartians/lefthook@latest +# Setup: lefthook install + +pre-commit: + parallel: true + commands: + gofmt: + glob: "*.go" + run: gofmt -l -w {staged_files} + stage_fixed: true + + govet: + glob: "*.go" + run: go vet ./... + +commit-msg: + commands: + commitlint: + run: | + msg=$(cat {1}) + if ! echo "$msg" | grep -qE "^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?(!)?: .+"; then + echo "Error: Commit message must follow conventional commits format" + echo "Examples:" + echo " feat: add new feature" + echo " fix(api): resolve null pointer" + echo " docs: update README" + exit 1 + fi