From 91c7d5bcf70dec36e6aee33cbfeaa12dc05be5a3 Mon Sep 17 00:00:00 2001 From: Lucas Machado Date: Sun, 8 Mar 2026 15:15:33 +0100 Subject: [PATCH] ci: add PR title conventional commit validation Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pr.yml | 75 ++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 33 ++++++++++++--- .github/workflows/review.yml | 26 ------------ .github/workflows/validate.yml | 22 ---------- lefthook.yml | 2 +- 5 files changed, 104 insertions(+), 54 deletions(-) create mode 100644 .github/workflows/pr.yml delete mode 100644 .github/workflows/review.yml delete mode 100644 .github/workflows/validate.yml 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..8098fa5 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,38 @@ jobs: - name: Run tests run: go test ./... -v + - name: Determine version + id: version + uses: AxeForging/releaseforge@main + with: + command: bump + + - name: Set tag + id: tag + run: | + 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: | - git tag ${{ inputs.tag }} - git push origin ${{ inputs.tag }} + 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: version: latest - args: release --clean + 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/validate.yml b/.github/workflows/validate.yml deleted file mode 100644 index 3f93efd..0000000 --- a/.github/workflows/validate.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Validate Structure - -on: - pull_request: - types: [opened, synchronize, reopened] - branches: - - main - -permissions: - contents: read - pull-requests: write - -jobs: - structlint: - 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/lefthook.yml b/lefthook.yml index 7cc9140..8a6f0a1 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -27,7 +27,7 @@ commit-msg: run: | msg=$(cat {1}) # Check for conventional commit format - if ! echo "$msg" | grep -qE "^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?: .+"; then + 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"