diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100644 index 0000000..409f51d --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,27 @@ +#!/bin/bash + +commit_msg_file=$1 +commit_msg=$(cat "$commit_msg_file") + +# Allow merge commits +if echo "$commit_msg" | grep -qE "^Merge "; then + exit 0 +fi + +# Pattern: [()][!]: +if ! echo "$commit_msg" | grep -qE "^(feat|fix|chore|hotfix|docs)(\([a-z0-9/-]+\))?(!)?: .+$"; then + echo "" + echo "❌ BLOCKED: Commit message does not follow convention." + echo "" + echo "✅ Format: [()]: " + echo "" + echo "📌 Types: feat | fix | chore | hotfix | docs" + echo "" + echo "📌 Examples:" + echo " feat(deploy): add Cloud Run job support" + echo " fix(build): resolve duplicate step names" + echo " chore: update action pin hashes" + echo " docs: update README with new inputs" + echo "" + exit 1 +fi diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100644 index 0000000..0cda404 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,31 @@ +#!/bin/bash + +branch=$(git symbolic-ref --short HEAD 2>/dev/null) + +if [[ -z "$branch" ]]; then + echo "⚠️ Could not determine branch name (detached HEAD?). Skipping check." + exit 0 +fi + +if [[ "$branch" == "main" ]]; then + echo "❌ BLOCKED: Direct pushes to 'main' are not allowed." + echo "" + echo "✅ Create a feature/fix branch and open a PR instead:" + echo " git checkout -b feat/" + echo " git push origin feat/" + echo " # Then open a Pull Request on GitHub" + exit 1 +fi + +if ! [[ "$branch" =~ ^(main|(feat|fix|chore|hotfix|docs)/.+)$ ]]; then + echo "❌ BLOCKED: Branch '$branch' does not follow naming convention." + echo "" + echo "✅ Allowed formats:" + echo " main | /" + echo " where ∈ feat | fix | chore | hotfix | docs" + echo "" + echo "📌 Examples:" + echo " git checkout -b feat/docker-build-cloudrun-actions" + echo " git checkout -b fix/scheduler-reconcile" + exit 1 +fi diff --git a/.github/workflows/branch-protection.yml b/.github/workflows/branch-protection.yml new file mode 100644 index 0000000..21dcd4d --- /dev/null +++ b/.github/workflows/branch-protection.yml @@ -0,0 +1,96 @@ +name: Branch Protection Rules + +on: + push: + branches: + - main + +jobs: + revert-and-notify: + runs-on: ubuntu-latest + if: github.actor != 'github-actions[bot]' + environment: prod + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Check if merge commit + id: merge_check + run: | + PARENT_COUNT=$(git log -1 --pretty=format:%P | wc -w) + if [ "$PARENT_COUNT" -gt 1 ]; then + echo "is_merge=true" >> "$GITHUB_OUTPUT" + else + echo "is_merge=false" >> "$GITHUB_OUTPUT" + fi + + - name: Get actor real name + if: steps.merge_check.outputs.is_merge == 'false' + id: actor_info + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + REAL_NAME=$(gh api users/${{ github.actor }} --jq '.name') + if [ -z "$REAL_NAME" ] || [ "$REAL_NAME" = "null" ]; then + REAL_NAME="${{ github.actor }}" + fi + echo "name=$REAL_NAME" >> $GITHUB_OUTPUT + + - name: Get commit title + if: steps.merge_check.outputs.is_merge == 'false' + id: commit_info + run: | + COMMIT_TITLE=$(git log -1 --pretty=format:%s | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') + echo "title=$COMMIT_TITLE" >> "$GITHUB_OUTPUT" + + - name: Revert push to ${{ github.ref_name }} + if: steps.merge_check.outputs.is_merge == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git revert HEAD --no-edit + git push origin ${{ github.ref_name }} + + - name: Notify Slack + if: steps.merge_check.outputs.is_merge == 'false' + uses: slackapi/slack-github-action@v2.1.1 + with: + webhook-type: incoming-webhook + payload: | + { + "attachments": [ + { + "color": "#ff0000", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":x: *Direct Commit to `${{ github.ref_name }}` Branch Not Allowed*\n\n*Pushed By:* `${{ steps.actor_info.outputs.name }}`\n*Repository:* `${{ github.event.repository.name }}`\n*Commit Message:* <${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ steps.commit_info.outputs.title }}>\n\n> :memo: *Note:* Please use a feature branch and open a Pull Request instead." + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Pipeline Run" + }, + "style": "danger", + "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + ] + } + ] + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..023bfa3 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: setup + +## setup : Configure Git hooks from .githooks/ +setup: + @echo "Configuring Git hooks..." + @git config core.hooksPath .githooks + @echo "Done. Hooks active."