diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 0000000..07f1cf8 --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,44 @@ +version: 2 +mergeable: + - when: pull_request.*, pull_request_review.* + validate: + # Validate PR title + - do: title + must_include: + regex: '^(feat|docs|chore|fix|refactor|test|style|perf)(\(\w+\))?: .{5,}' + message: "Semantic release conventions must be followed. Example: feat(auth): add login page. Title must be at least 5 characters after the prefix." + + # Ensure PR description is provided + - do: description + must_include: + regex: "[\\s\\S]{20,}" # At least 20 characters + message: "Please provide a meaningful description of the PR (minimum 20 characters)." + + # Ensure PR references an associated issue + - do: description + must_include: + regex: "(Closes|Fixes|Resolves|Addresses)\\s+#[0-9]+(,?\\s*#[0-9]+)*" + message: "PR must reference at least one issue (e.g., Closes #123, Fixes #123, #124)." + + # Ensure at least one required label is applied + - do: label + must_include: + regex: "^(bug|enhancement|documentation|feature|refactor|performance|chore|wip|test|ci|security|dependencies)$" + message: "PR must include at least one valid label." + # Ensure PR has at least one assignee + - do: assignee + min: + count: 1 + message: "PR must have at least one assignee." + # Ensure PR has at least one reviewer requested + - do: approvals + min: + count: 1 + message: "PR must have at least one reviewer requested or approved." + + pass: + - do: labels + add: + - "validated" + - do: checks + status: "success" diff --git a/.github/workflows/analysis.yaml b/.github/workflows/analysis.yaml new file mode 100644 index 0000000..8c1f2ad --- /dev/null +++ b/.github/workflows/analysis.yaml @@ -0,0 +1,27 @@ +name: Analysis +on: + workflow_call: + secrets: + SONAR_TOKEN: + required: true + SONAR_ORGANIZATION: + required: true + SONAR_PROJECT_KEY: + required: true + +jobs: + analysis: + name: Analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: SonarCloud Analysis + uses: SonarSource/sonarqube-scan-action@v3.0.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: > + -Dsonar.organization=${{ secrets.SONAR_ORGANIZATION }} + -Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }} diff --git a/.github/workflows/notifications.yaml b/.github/workflows/notifications.yaml new file mode 100644 index 0000000..0ecaa8f --- /dev/null +++ b/.github/workflows/notifications.yaml @@ -0,0 +1,227 @@ +name: Notify (GitHub Events) + +on: + workflow_call: + inputs: + workflow_name: + description: 'Name of the workflow that triggered this' + required: false + type: string + default: 'GitHub Event' + secrets: + GOOGLE_CHAT_WEBHOOK: + required: true + SLACK_BOT_TOKEN: + required: true + SLACK_CHANNEL_ID: + required: true + issues: + types: [opened, edited, reopened, closed, assigned, labeled] + pull_request: + types: [opened, closed, reopened, ready_for_review, review_requested] + pull_request_review: + types: [submitted, edited, dismissed] + issue_comment: + types: [created, edited] + pull_request_review_comment: + types: [created, edited] + +jobs: + notify: + runs-on: ubuntu-latest + name: Send Notifications + steps: + - name: Determine Event Type + id: event_info + run: | + EVENT="${{ github.event_name }}" + ACTION="${{ github.event.action }}" + REPO="${{ github.repository }}" + ACTOR="${{ github.actor }}" + REPO_URL="${{ github.server_url }}/${{ github.repository }}" + + if [[ "$EVENT" == "issues" ]]; then + TITLE="Issue $ACTION: ${{ github.event.issue.title }}" + URL="${{ github.event.issue.html_url }}" + NUMBER="${{ github.event.issue.number }}" + USER="${{ github.event.issue.user.login }}" + DESCRIPTION="Issue #$NUMBER by $USER" + STATE="${{ github.event.issue.state }}" + EMOJI="🐛" + + elif [[ "$EVENT" == "pull_request" ]]; then + NUMBER="${{ github.event.pull_request.number }}" + USER="${{ github.event.pull_request.user.login }}" + URL="${{ github.event.pull_request.html_url }}" + STATE="${{ github.event.pull_request.state }}" + SOURCE="${{ github.event.pull_request.head.ref }}" + TARGET="${{ github.event.pull_request.base.ref }}" + + if [[ "$ACTION" == "closed" && "${{ github.event.pull_request.merged }}" == "true" ]]; then + TITLE="PR merged: ${{ github.event.pull_request.title }}" + EMOJI="✅" + STATE="merged" + elif [[ "$ACTION" == "closed" ]]; then + TITLE="PR closed: ${{ github.event.pull_request.title }}" + EMOJI="❌" + else + TITLE="PR $ACTION: ${{ github.event.pull_request.title }}" + EMOJI="🔀" + fi + DESCRIPTION="PR #$NUMBER by $USER: $SOURCE → $TARGET" + + elif [[ "$EVENT" == "pull_request_review" ]]; then + TITLE="PR Review: ${{ github.event.pull_request.title }}" + URL="${{ github.event.review.html_url }}" + NUMBER="${{ github.event.pull_request.number }}" + USER="${{ github.event.review.user.login }}" + REVIEW_STATE="${{ github.event.review.state }}" + STATE="${{ github.event.review.state }}" + SOURCE="${{ github.event.pull_request.head.ref }}" + TARGET="${{ github.event.pull_request.base.ref }}" + DESCRIPTION="Review by $USER on PR #$NUMBER" + EMOJI="👀" + + elif [[ "$EVENT" == "issue_comment" ]]; then + NUMBER="${{ github.event.issue.number }}" + USER="${{ github.event.comment.user.login }}" + URL="${{ github.event.comment.html_url }}" + STATE="commented" + + if [[ "${{ github.event.issue.pull_request }}" != "" ]]; then + TITLE="Comment on PR: ${{ github.event.issue.title }}" + EMOJI="💬" + else + TITLE="Comment on Issue: ${{ github.event.issue.title }}" + EMOJI="💬" + fi + DESCRIPTION="Comment by $USER on #$NUMBER" + + elif [[ "$EVENT" == "pull_request_review_comment" ]]; then + TITLE="Comment on PR: ${{ github.event.pull_request.title }}" + URL="${{ github.event.comment.html_url }}" + NUMBER="${{ github.event.pull_request.number }}" + USER="${{ github.event.comment.user.login }}" + STATE="commented" + SOURCE="${{ github.event.pull_request.head.ref }}" + TARGET="${{ github.event.pull_request.base.ref }}" + DESCRIPTION="Review comment by $USER on PR #$NUMBER" + EMOJI="💬" + else + TITLE="GitHub Event: $EVENT" + URL="${{ github.event.repository.html_url }}" + DESCRIPTION="Event triggered in $REPO" + STATE="N/A" + NUMBER="N/A" + USER="$ACTOR" + EMOJI="📢" + fi + + echo "title=$TITLE" >> $GITHUB_OUTPUT + echo "url=$URL" >> $GITHUB_OUTPUT + echo "description=$DESCRIPTION" >> $GITHUB_OUTPUT + echo "emoji=$EMOJI" >> $GITHUB_OUTPUT + echo "number=${NUMBER:-N/A}" >> $GITHUB_OUTPUT + echo "user=${USER:-$ACTOR}" >> $GITHUB_OUTPUT + echo "state=${STATE:-N/A}" >> $GITHUB_OUTPUT + echo "source=${SOURCE:-N/A}" >> $GITHUB_OUTPUT + echo "target=${TARGET:-N/A}" >> $GITHUB_OUTPUT + echo "repo_url=$REPO_URL" >> $GITHUB_OUTPUT + + - name: Google Chat Notification + if: always() + uses: Co-qn/google-chat-notification@releases/v1 + with: + name: ${{ steps.event_info.outputs.title }} + url: ${{ secrets.GOOGLE_CHAT_WEBHOOK }} + status: ${{ job.status }} + + - name: Slack Notification + if: always() + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + payload: | + { + "text": "${{ steps.event_info.outputs.title }}", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "${{ steps.event_info.outputs.emoji }} ${{ steps.event_info.outputs.title }}" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Repository:*\n<${{ steps.event_info.outputs.repo_url }}|${{ github.repository }}>" + }, + { + "type": "mrkdwn", + "text": "*Event:*\n${{ github.event_name }}" + }, + { + "type": "mrkdwn", + "text": "*Author:*\n${{ steps.event_info.outputs.user }}" + }, + { + "type": "mrkdwn", + "text": "*Action:*\n${{ github.event.action }}" + }, + { + "type": "mrkdwn", + "text": "*Number:*\n<${{ steps.event_info.outputs.url }}|#${{ steps.event_info.outputs.number }}>" + }, + { + "type": "mrkdwn", + "text": "*State:*\n${{ steps.event_info.outputs.state }}" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "${{ steps.event_info.outputs.description }}" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View on GitHub", + "emoji": true + }, + "url": "${{ steps.event_info.outputs.url }}", + "style": "primary" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Repository", + "emoji": true + }, + "url": "${{ steps.event_info.outputs.repo_url }}" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Triggered by ${{ github.actor }} • ${{ github.event_name }} event" + } + ] + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 0000000..3728af0 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,119 @@ +name: Pre-Commit Checks + +on: + workflow_call: # Trigger for reusable workflows + +jobs: + precommit: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install pre-commit + run: | + python -m pip install --upgrade pip + pip install pre-commit + + - name: Load Pre-commit Config + run: | + if [ ! -f ".pre-commit-config.yaml" ]; then + echo " No .pre-commit-config.yaml found — downloading BerryBytes global config..." + curl -sSL \ + https://raw.githubusercontent.com/BerryBytes/precommit-util/main/global/precommitFile/.pre-commit-config.yaml \ + -o .pre-commit-config.yaml + else + echo "✔ Using project's existing .pre-commit-config.yaml" + fi + + - name: Inject temporary Stylelint config for CI + run: | + if [ ! -f ".stylelintrc.json" ]; then + echo " Creating temporary .stylelintrc.json for CI..." + cat < .stylelintrc.json + { + "extends": "stylelint-config-standard", + "rules": { + "no-duplicate-selectors": true, + "color-hex-length": "short", + "selector-no-qualifying-type": true, + "selector-max-id": 0 + } + } + EOF + else + echo "✔ .stylelintrc.json already exists — skipping" + fi + + # -------------------------------------------------------------------- + # STEP 1: Run pre-commit (capture full logs and exit code safely) + # -------------------------------------------------------------------- + - name: Run pre-commit (full logs) + id: runprecommit + run: | + echo "🔍 Running full pre-commit checks..." + + set +e # allow failure + pre-commit run --all-files --verbose --show-diff-on-failure --color never \ + | tee full_precommit.log + exit_code=${PIPESTATUS[0]} + + echo "Pre-commit exit code: $exit_code" + echo "$exit_code" > precommit_exit_code.txt + + # -------------------------------------------------------------------- + # STEP 2: Summary of FAILED hooks + # -------------------------------------------------------------------- + - name: Pre-commit summary of failed hooks + run: | + echo "=====================================================" + echo " PRE-COMMIT SUMMARY" + echo "=====================================================" + + exit_code=$(cat precommit_exit_code.txt) + + if [ "$exit_code" = "0" ]; then + echo " All hooks passed!" + exit 0 + fi + + echo " Hooks failed — showing summary:" + echo "" + + echo " FAILED HOOKS:" + grep -E "^\w.*\.{3,}Failed" full_precommit.log || echo " None" + echo "-----------------------------------------------------" + + echo " FILES WITH ISSUES:" + grep -E "files were modified by this hook" -A3 full_precommit.log \ + | sed 's/^/ - /' || echo " None" + echo "-----------------------------------------------------" + + echo " ERROR DETAILS:" + grep -Ei "(error|failed|violation|missing|line too long|could not|warning)" full_precommit.log \ + | grep -Ev "^(---|\+\+\+|@@|diff --git|index )" \ + | sed 's/^/ • /' || echo " None" + echo "-----------------------------------------------------" + + exit $exit_code + # -------------------------------------------------------------------- + # STEP 3: Run pre-commit skipping local Go hooks (for CI efficiency) + # -------------------------------------------------------------------- + - name: Run pre-commit (skip local Go hooks in CI) + id: precommit_skip_go + env: + SKIP: go-fmt,go-vet,go-imports,golangci-lint # This disables all local Go hooks in CI only + run: | + echo "Running pre-commit (Go hooks skipped in CI via SKIP env var)" + set +e + pre-commit run --all-files --verbose --show-diff-on-failure --color never \ + | tee full_precommit.log + exit_code=${PIPESTATUS[0]} + echo "Pre-commit exit code: $exit_code" + echo "$exit_code" > precommit_exit_code.txt diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 0000000..c671f54 --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,141 @@ +name: Stale PRs and Issues Management +permissions: + actions: write + contents: write + issues: write + pull-requests: write + +on: + workflow_call: + inputs: + # General settings + days-before-stale: + description: 'Days before marking as stale (applies to both issues and PRs unless overridden)' + required: false + default: '30' + type: string + days-before-close: + description: 'Days before closing stale items (applies to both issues and PRs unless overridden)' + required: false + default: '7' + type: string + + # Issue-specific overrides + days-before-issue-stale: + description: 'Days before marking issues as stale (overrides days-before-stale)' + required: false + type: string + days-before-issue-close: + description: 'Days before closing stale issues (overrides days-before-close)' + required: false + type: string + + # PR-specific overrides + days-before-pr-stale: + description: 'Days before marking PRs as stale (overrides days-before-stale)' + required: false + type: string + days-before-pr-close: + description: 'Days before closing stale PRs (overrides days-before-close)' + required: false + type: string + + # PR labels and messages + stale-pr-label: + description: 'Label for stale PRs' + required: false + default: 'stale' + type: string + # NOTE: Avoid hardcoding numeric durations in message defaults. + # If you customize `days-before-pr-close` when calling this workflow, + # update this message accordingly or keep it generic so it won't contradict + # the configured closing period. + stale-pr-message: + description: 'Message for stale PRs (avoid hardcoding durations; prefer generic wording or sync with days-before-pr-close)' + required: false + default: 'This PR has been marked as stale due to inactivity and may be closed within the configured period unless further changes are made or a review is requested.' + type: string + close-pr-message: + description: 'Message for closing stale PRs' + required: false + default: 'This PR has been closed due to inactivity. Please feel free to reopen if you think it is still relevant.' + type: string + + # Issue labels and messages + stale-issue-label: + description: 'Label for stale issues' + required: false + default: 'stale' + type: string + # NOTE: Avoid hardcoding numeric durations in message defaults. + # If you customize `days-before-issue-close` when calling this workflow, + # update this message accordingly or keep it generic so it won't contradict + # the configured closing period. + stale-issue-message: + description: 'Message for stale issues (avoid hardcoding durations; prefer generic wording or sync with days-before-issue-close)' + required: false + default: 'This issue has been marked as stale due to inactivity and may be closed within the configured period unless there is further activity.' + type: string + close-issue-message: + description: 'Message for closing stale issues' + required: false + default: 'This issue has been closed due to inactivity. Please feel free to reopen if you think it is still relevant.' + type: string + + # Exemptions + exempt-pr-labels: + description: 'Labels to exempt PRs from being marked as stale' + required: false + default: 'WIP' + type: string + exempt-issue-labels: + description: 'Labels to exempt issues from being marked as stale' + required: false + default: 'bug,critical,enhancement,security' + type: string + + # Additional options + operations-per-run: + description: 'Maximum number of operations per run' + required: false + default: '30' + type: string + remove-stale-when-updated: + description: 'Remove stale label when issue/PR is updated' + required: false + default: 'true' + type: string + +jobs: + stale-management: + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Manage Stale Issues and PRs + uses: actions/stale@v10.1.0 + with: + repo-token: ${{ github.token }} + + # General settings + days-before-stale: ${{ inputs.days-before-stale }} + days-before-close: ${{ inputs.days-before-close }} + operations-per-run: ${{ inputs.operations-per-run }} + remove-stale-when-updated: ${{ inputs.remove-stale-when-updated }} + + # Issue-specific settings + days-before-issue-stale: ${{ inputs.days-before-issue-stale }} + days-before-issue-close: ${{ inputs.days-before-issue-close }} + stale-issue-label: ${{ inputs.stale-issue-label }} + stale-issue-message: ${{ inputs.stale-issue-message }} + close-issue-message: ${{ inputs.close-issue-message }} + exempt-issue-labels: ${{ inputs.exempt-issue-labels }} + + # PR-specific settings + days-before-pr-stale: ${{ inputs.days-before-pr-stale }} + days-before-pr-close: ${{ inputs.days-before-pr-close }} + stale-pr-label: ${{ inputs.stale-pr-label }} + stale-pr-message: ${{ inputs.stale-pr-message }} + close-pr-message: ${{ inputs.close-pr-message }} + exempt-pr-labels: ${{ inputs.exempt-pr-labels }} diff --git a/README.md b/README.md index 852319c..32073ac 100644 --- a/README.md +++ b/README.md @@ -1 +1,51 @@ -Workflow +### Repository of Reusable GitHub Actions Workflows + +--- + +# Reusable GitHub Actions Workflows + +Welcome to our repository of reusable GitHub Actions workflows. These workflows are designed to be easily integrated into any GitHub repository to automate various aspects of software development and maintenance processes. + +## Available Workflows + +Currently, the repository hosts the following reusable workflows: +- **SonarCloud Analysis**: Automates the code quality checking using SonarCloud. +- **Notification**: Github Event Notification in Google WorkSpace and Slack Channel . +- **Stale Issue/PR Handler**: Manages inactive issues and pull requests to keep project boards clean and updated. + +## How to Use the Workflows + +### Prerequisites + +Before using these workflows, make sure you have: +- A GitHub account. +- Required permissions to add workflows and secrets to your GitHub repositories. + +### General Usage Steps + +1. **Choose a Workflow**: Decide which workflow from this repository you want to use. + +2. **Add Secrets (if required)**: Some workflows might require you to set up repository secrets. For instance, the SonarCloud Analysis needs secrets like `SONAR_TOKEN`. You can add these secrets by navigating to: + ``` + Settings > Secrets > Actions > New repository secret + ``` + +3. **Import the Workflow**: To utilize a workflow, add it to a `.yml` file in the `.github/workflows` directory of your repository. For example, to use the SonarCloud Analysis, you might write: + + ```yaml + name: SonarCloud Analysis + on: + push: + branches: + - main + - develop + pull_request: + + jobs: + sonarcloud: + uses: berrybytes/workflows/.github/workflows/sonarcloud-analysis.yml@main + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_ORGANIZATION: ${{ secrets.SONAR_ORGANIZATION }} + SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }} + ``` diff --git a/commitlint.config.cjs b/commitlint.config.cjs new file mode 100644 index 0000000..1cec015 --- /dev/null +++ b/commitlint.config.cjs @@ -0,0 +1,24 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [2, 'always', [ + 'feat', // A new feature + 'fix', // A bug fix + 'docs', // Documentation only changes + 'style', // Changes that do not affect code meaning + 'refactor', // A code change that neither fixes a bug nor adds a feature + 'perf', // A code change that improves performance + 'test', // Adding missing tests or correcting existing tests + 'build', // Build system or external dependencies + 'ci', // CI configuration changes + 'chore', // Other changes that don't modify src or test files + 'revert', // Reverts a previous commit + ]], + 'type-case': [2, 'always', 'lower-case'], + 'type-empty': [2, 'never'], + 'scope-case': [2, 'always', 'lower-case'], + 'subject-empty': [2, 'never'], + 'subject-full-stop': [2, 'never', '.'], + 'header-max-length': [2, 'always', 72], + }, +};