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 5c5c014..19ea45d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,8 +4,8 @@ on: workflow_dispatch: inputs: tag: - description: 'Release tag (e.g., v1.0.0)' - required: true + description: 'Release tag (leave empty for auto-bump from conventional commits)' + required: false type: string permissions: @@ -29,15 +29,40 @@ jobs: - name: Run tests run: go test ./... -v + - name: Build releaseforge + run: make build-local + + - name: Determine version + id: version + run: | + if [ -n "${{ inputs.tag }}" ]; then + echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" + else + NEXT=$(./releaseforge bump --quiet) + echo "Auto-bumped version: ${NEXT}" + echo "tag=${NEXT}" >> "$GITHUB_OUTPUT" + fi + + - name: Generate release notes + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + run: | + PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + ARGS="generate --use-git-fallback --output /tmp/release-notes.md" + if [ -n "$PREV_TAG" ]; then + ARGS="$ARGS --git-tag $PREV_TAG --analyze-from-tag" + fi + ./releaseforge $ARGS + - name: Create and push tag run: | - git tag ${{ inputs.tag }} - git push origin ${{ inputs.tag }} + git tag ${{ steps.version.outputs.tag }} + git push origin ${{ steps.version.outputs.tag }} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: version: latest - args: release --clean + args: release --clean --release-notes /tmp/release-notes.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/releaseforge.yml b/.github/workflows/releaseforge.yml new file mode 100644 index 0000000..2289a03 --- /dev/null +++ b/.github/workflows/releaseforge.yml @@ -0,0 +1,66 @@ +name: ReleaseForge + +on: + workflow_call: + inputs: + command: + description: "Command: bump, generate" + type: string + default: "bump" + tag: + description: "Base semver tag" + type: string + default: "" + branch: + description: "Target branch" + type: string + default: "HEAD" + provider: + description: "LLM provider for generate" + type: string + default: "gemini" + model: + description: "LLM model for generate" + type: string + default: "gemini-2.0-flash" + template-name: + description: "Built-in template name" + type: string + default: "" + max-commits: + description: "Max commits to analyze" + type: string + default: "200" + secrets: + api_key: + description: "LLM API key (for generate command)" + required: false + outputs: + next-version: + description: "Next semver version" + value: ${{ jobs.releaseforge.outputs.next-version }} + +permissions: + contents: read + +jobs: + releaseforge: + runs-on: ubuntu-latest + outputs: + next-version: ${{ steps.rf.outputs.next-version }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: rf + uses: AxeForging/releaseforge@main + with: + command: ${{ inputs.command }} + tag: ${{ inputs.tag }} + branch: ${{ inputs.branch }} + provider: ${{ inputs.provider }} + model: ${{ inputs.model }} + api-key: ${{ secrets.api_key }} + template-name: ${{ inputs.template-name }} + max-commits: ${{ inputs.max-commits }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a76d97a..461f70b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,18 +2,20 @@ 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 with: fetch-depth: 0 - - name: Set up Go + - name: Setup Go uses: actions/setup-go@v5 with: go-version: '1.24' diff --git a/.goreleaser.yml b/.goreleaser.yml index 81b4de7..2e259ae 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -46,11 +46,7 @@ snapshot: version_template: "{{ incpatch .Version }}-next" changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' + disable: true release: github: 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 new file mode 100644 index 0000000..e99d255 --- /dev/null +++ b/action.yml @@ -0,0 +1,137 @@ +name: "ReleaseForge" +description: "Automated version bumping and AI-powered release notes from conventional commits" +author: "AxeForging" +inputs: + command: + description: "Command to run: bump, generate" + required: true + default: "bump" + tag: + description: "Base semver tag to compare against (auto-detects latest if omitted)" + required: false + branch: + description: "Target branch or ref to compare (default: HEAD)" + required: false + default: "HEAD" + quiet: + description: "Only output the version string (bump command)" + required: false + default: "true" + provider: + description: "LLM provider for generate: gemini, openai, anthropic" + required: false + default: "gemini" + model: + description: "LLM model name for generate" + required: false + default: "gemini-2.0-flash" + api-key: + description: "API key for the LLM provider (generate command)" + required: false + template-name: + description: "Built-in template: semver-release-notes, conventional-changelog, version-analysis" + required: false + use-git-fallback: + description: "Fall back to git commit log analysis if LLM fails" + required: false + default: "true" + max-commits: + description: "Maximum number of commits to analyze" + required: false + default: "200" + version: + description: "Version of releaseforge to use (defaults to latest)" + required: false + +outputs: + next-version: + description: "The next semver version (from bump command)" + value: ${{ steps.run.outputs.next_version }} + release-notes: + description: "Path to generated release notes file (from generate command)" + value: ${{ steps.run.outputs.release_notes }} + +runs: + using: "composite" + steps: + - name: Download ReleaseForge + shell: bash + run: | + OS="linux" + ARCH="amd64" + if [[ "${{ runner.os }}" == "macOS" ]]; then OS="darwin"; fi + if [[ "${{ runner.arch }}" == "ARM64" ]]; then ARCH="arm64"; fi + + VERSION="${{ inputs.version }}" + if [ -z "$VERSION" ]; then + VERSION="${{ github.action_ref }}" + fi + if [ -z "$VERSION" ] || [ "$VERSION" == "main" ]; then + # Fetch latest release tag + VERSION=$(curl -sS -o /dev/null -w '%{redirect_url}' \ + "https://github.com/AxeForging/releaseforge/releases/latest" | grep -oP '[^/]+$') + echo "Resolved latest version: ${VERSION}" + fi + + URL="https://github.com/AxeForging/releaseforge/releases/download/${VERSION}/releaseforge-${OS}-${ARCH}.tar.gz" + echo "Downloading releaseforge ${VERSION} from ${URL}..." + curl -sSL "$URL" -o /tmp/releaseforge.tar.gz + tar -xzf /tmp/releaseforge.tar.gz -C /tmp + chmod +x /tmp/releaseforge + echo "releaseforge ${VERSION} installed successfully" + + - name: Run ReleaseForge + id: run + shell: bash + env: + GEMINI_API_KEY: ${{ inputs.api-key }} + OPENAI_API_KEY: ${{ inputs.api-key }} + ANTHROPIC_API_KEY: ${{ inputs.api-key }} + run: | + CMD="${{ inputs.command }}" + + if [ "$CMD" = "bump" ]; then + ARGS="bump" + if [ -n "${{ inputs.tag }}" ]; then + ARGS="$ARGS --tag ${{ inputs.tag }}" + fi + ARGS="$ARGS --branch ${{ inputs.branch }}" + ARGS="$ARGS --max-commits ${{ inputs.max-commits }}" + + if [ "${{ inputs.quiet }}" = "true" ]; then + NEXT_VERSION=$(/tmp/releaseforge $ARGS --quiet) + echo "next_version=${NEXT_VERSION}" >> "$GITHUB_OUTPUT" + echo "Next version: ${NEXT_VERSION}" + else + /tmp/releaseforge $ARGS + NEXT_VERSION=$(/tmp/releaseforge $ARGS --quiet) + echo "next_version=${NEXT_VERSION}" >> "$GITHUB_OUTPUT" + fi + + elif [ "$CMD" = "generate" ]; then + ARGS="generate" + ARGS="$ARGS --provider ${{ inputs.provider }}" + ARGS="$ARGS --model ${{ inputs.model }}" + ARGS="$ARGS --max-commits ${{ inputs.max-commits }}" + + if [ -n "${{ inputs.tag }}" ]; then + ARGS="$ARGS --git-tag ${{ inputs.tag }} --analyze-from-tag" + fi + + if [ -n "${{ inputs.template-name }}" ]; then + ARGS="$ARGS --template-name ${{ inputs.template-name }}" + fi + + if [ "${{ inputs.use-git-fallback }}" = "true" ]; then + ARGS="$ARGS --use-git-fallback" + fi + + ARGS="$ARGS --output /tmp/release-notes.md" + /tmp/releaseforge $ARGS + echo "release_notes=/tmp/release-notes.md" >> "$GITHUB_OUTPUT" + echo "Release notes generated at /tmp/release-notes.md" + + else + echo "::error::Unknown command: $CMD. Use 'bump' or 'generate'." + exit 1 + fi 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