diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e82b5f4..2b0d3b9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,6 +2,7 @@ /.github/workflows/ci.yml @victormgomes /.github/workflows/docs.yml @victormgomes /.github/workflows/auto-approve.yml @victormgomes +/.github/workflows/branch-cleanup.yml @victormgomes /.github/dependabot.yml @victormgomes /.github/PULL_REQUEST_TEMPLATE.md @victormgomes /.github/ISSUE_TEMPLATE/ @victormgomes @@ -14,13 +15,6 @@ /.gitattributes @victormgomes /.gitignore @victormgomes /.releaserc.json @victormgomes -/captainhook.json @victormgomes -/composer-require-checker.json @victormgomes -/infection.json.dist @victormgomes -/phpinsights.php @victormgomes -/phpstan.neon.dist @victormgomes -/phpunit.xml.dist @victormgomes -/rector.php @victormgomes /package.json @victormgomes # DevOps & Docker diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b504706..cf3f151 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -13,10 +13,10 @@ effort. Small fixes (typos, broken links) can go directly to a Pull Request. ### 2. Fork or Branch -| Your access | Workflow | -|---|---| -| **External contributor** | Fork the repository, create a feature branch in your fork | -| **Trusted collaborator** (write access) | Create a feature branch directly in the main repository | +| Your access | Workflow | +| --------------------------------------- | --------------------------------------------------------- | +| **External contributor** | Fork the repository, create a feature branch in your fork | +| **Trusted collaborator** (write access) | Create a feature branch directly in the main repository | ### 3. Create a Feature Branch diff --git a/.github/workflows/branch-cleanup.yml b/.github/workflows/branch-cleanup.yml index 8ad647e..9d992f3 100644 --- a/.github/workflows/branch-cleanup.yml +++ b/.github/workflows/branch-cleanup.yml @@ -1,152 +1,152 @@ name: Branch Cleanup on: - workflow_call: - inputs: - age_days: - description: "Delete stale branches with no commits in N days" - required: false - type: number - default: 14 - max_deletions: - description: "Hard cap on branches deleted per run" - required: false - type: number - default: 50 - schedule: - - cron: "0 4 * * 1" # Every Monday at 04:00 UTC - workflow_dispatch: - inputs: - age_days: - description: "Delete stale branches with no commits in N days" - required: false - type: number - default: 14 - max_deletions: - description: "Hard cap on branches deleted per run" - required: false - type: number - default: 50 + workflow_call: + inputs: + age_days: + description: "Delete stale branches with no commits in N days" + required: false + type: number + default: 14 + max_deletions: + description: "Hard cap on branches deleted per run" + required: false + type: number + default: 50 + schedule: + - cron: "0 4 * * 1" # Every Monday at 04:00 UTC + workflow_dispatch: + inputs: + age_days: + description: "Delete stale branches with no commits in N days" + required: false + type: number + default: 14 + max_deletions: + description: "Hard cap on branches deleted per run" + required: false + type: number + default: 50 permissions: - contents: write - pull-requests: write - issues: write + contents: write + pull-requests: write + issues: write jobs: - cleanup: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Classify and clean branches - env: - GH_TOKEN: ${{ github.token }} - REPO: ${{ github.repository }} - AGE_DAYS: ${{ inputs.age_days }} - MAX_DELETIONS: ${{ inputs.max_deletions }} - run: | - { - echo "## Branch Cleanup Report" - echo "" - } >> "$GITHUB_STEP_SUMMARY" - - # Fetch all branches with last commit date - gh api "repos/$REPO/branches?per_page=100" --jq '.[] | "\(.name)\t\(.commit.sha)"' > /tmp/branches.txt - - # Fetch all open PRs (collect head branch names) - gh pr list --repo "$REPO" --state open --json headRefName --jq '.[].headRefName' | sort -u > /tmp/open_prs.txt - - # Fetch all merged PRs (collect head branch names) - gh pr list --repo "$REPO" --state merged --json headRefName --jq '.[].headRefName' | sort -u > /tmp/merged_prs.txt - - # Get default branch - DEFAULT_BRANCH=$(gh api "repos/$REPO" --jq '.default_branch') - - # Get protected branches - gh api "repos/$REPO/branches?per_page=100" --jq '.[] | select(.protected == true) | .name' > /tmp/protected_branches.txt - - NOW=$(date +%s) - DELETED=0 - ISSUED=0 - SKIPPED=0 - - { - echo "| Branch | Status | Reason | Last Commit | Author |" - echo "|--------|--------|--------|-------------|--------|" - } >> "$GITHUB_STEP_SUMMARY" - - while IFS=$'\t' read -r BRANCH SHA; do - # Skip default branch - if [ "$BRANCH" = "$DEFAULT_BRANCH" ]; then - SKIPPED=$((SKIPPED + 1)) - echo "| \`$BRANCH\` | SKIP | default branch | - | - |" >> "$GITHUB_STEP_SUMMARY" - continue - fi - - # Skip protected branches - if grep -qxF "$BRANCH" /tmp/protected_branches.txt 2>/dev/null; then - SKIPPED=$((SKIPPED + 1)) - echo "| \`$BRANCH\` | SKIP | protected branch | - | - |" >> "$GITHUB_STEP_SUMMARY" - continue - fi - - # Skip branches with open PRs - if grep -qxF "$BRANCH" /tmp/open_prs.txt 2>/dev/null; then - SKIPPED=$((SKIPPED + 1)) - echo "| \`$BRANCH\` | SKIP | has open PR | - | - |" >> "$GITHUB_STEP_SUMMARY" - continue - fi - - # Get last commit date and author - COMMIT_DATE=$(gh api "repos/$REPO/commits/$SHA" --jq '.commit.author.date') - AUTHOR=$(gh api "repos/$REPO/commits/$SHA" --jq '.commit.author.name') - COMMIT_TS=$(date -d "$COMMIT_DATE" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$COMMIT_DATE" +%s 2>/dev/null) - AGE_DAYS_CALC=$(( (NOW - COMMIT_TS) / 86400 )) - - # Check if branch was merged via PR - if grep -qxF "$BRANCH" /tmp/merged_prs.txt 2>/dev/null; then - # Merged branch — auto-delete - if [ "$DELETED" -lt "$MAX_DELETIONS" ]; then - ENC=$(printf '%s' "$BRANCH" | jq -sRr @uri) - if gh api -X DELETE "repos/$REPO/git/refs/heads/$ENC" 2>/dev/null; then - DELETED=$((DELETED + 1)) - echo "| \`$BRANCH\` | DELETED | merged via PR | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" - else - SKIPPED=$((SKIPPED + 1)) - echo "| \`$BRANCH\` | SKIP | delete failed | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" - fi - else - SKIPPED=$((SKIPPED + 1)) - echo "| \`$BRANCH\` | DEFERRED | max deletions reached | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" - fi - continue - fi - - # Check if stale (no commits in N days) - if [ "$AGE_DAYS_CALC" -ge "$AGE_DAYS" ]; then - # Stale branch — create issue - if [ "$DELETED" -lt "$MAX_DELETIONS" ]; then - ISSUE_TITLE="Stale branch: \`$BRANCH\` (last commit $AGE_DAYS_CALC days ago)" - ISSUE_BODY="Branch \`$BRANCH\` has no open PR and no commits for $AGE_DAYS_CALC days (since $COMMIT_DATE).\n\nAuthor: $AUTHOR\n\nThis branch was automatically flagged for cleanup. If this branch is still needed, please comment within 7 days. Otherwise, it will be deleted in the next cleanup run." - gh issue create --repo "$REPO" --title "$ISSUE_TITLE" --body "$ISSUE_BODY" --label "stale-branch" 2>/dev/null && ISSUED=$((ISSUED + 1)) - SKIPPED=$((SKIPPED + 1)) - echo "| \`$BRANCH\` | ISSUE CREATED | stale ($AGE_DAYS_CALC days) | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" - else - SKIPPED=$((SKIPPED + 1)) - echo "| \`$BRANCH\` | DEFERRED | max deletions reached | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" - fi - continue - fi - - # Active branch — skip - SKIPPED=$((SKIPPED + 1)) - echo "| \`$BRANCH\` | SKIP | active ($AGE_DAYS_CALC days) | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" - - done < /tmp/branches.txt - - { - echo "" - echo "**Summary:** $DELETED deleted, $ISSUED issues created, $SKIPPED skipped" - echo "" - echo "Config: age_days=$AGE_DAYS, max_deletions=$MAX_DELETIONS" - } >> "$GITHUB_STEP_SUMMARY" + cleanup: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Classify and clean branches + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + AGE_DAYS: ${{ inputs.age_days }} + MAX_DELETIONS: ${{ inputs.max_deletions }} + run: | + { + echo "## Branch Cleanup Report" + echo "" + } >> "$GITHUB_STEP_SUMMARY" + + # Fetch all branches with last commit date + gh api "repos/$REPO/branches?per_page=100" --jq '.[] | "\(.name)\t\(.commit.sha)"' > /tmp/branches.txt + + # Fetch all open PRs (collect head branch names) + gh pr list --repo "$REPO" --state open --json headRefName --jq '.[].headRefName' | sort -u > /tmp/open_prs.txt + + # Fetch all merged PRs (collect head branch names) + gh pr list --repo "$REPO" --state merged --json headRefName --jq '.[].headRefName' | sort -u > /tmp/merged_prs.txt + + # Get default branch + DEFAULT_BRANCH=$(gh api "repos/$REPO" --jq '.default_branch') + + # Get protected branches + gh api "repos/$REPO/branches?per_page=100" --jq '.[] | select(.protected == true) | .name' > /tmp/protected_branches.txt + + NOW=$(date +%s) + DELETED=0 + ISSUED=0 + SKIPPED=0 + + { + echo "| Branch | Status | Reason | Last Commit | Author |" + echo "|--------|--------|--------|-------------|--------|" + } >> "$GITHUB_STEP_SUMMARY" + + while IFS=$'\t' read -r BRANCH SHA; do + # Skip default branch + if [ "$BRANCH" = "$DEFAULT_BRANCH" ]; then + SKIPPED=$((SKIPPED + 1)) + echo "| \`$BRANCH\` | SKIP | default branch | - | - |" >> "$GITHUB_STEP_SUMMARY" + continue + fi + + # Skip protected branches + if grep -qxF "$BRANCH" /tmp/protected_branches.txt 2>/dev/null; then + SKIPPED=$((SKIPPED + 1)) + echo "| \`$BRANCH\` | SKIP | protected branch | - | - |" >> "$GITHUB_STEP_SUMMARY" + continue + fi + + # Skip branches with open PRs + if grep -qxF "$BRANCH" /tmp/open_prs.txt 2>/dev/null; then + SKIPPED=$((SKIPPED + 1)) + echo "| \`$BRANCH\` | SKIP | has open PR | - | - |" >> "$GITHUB_STEP_SUMMARY" + continue + fi + + # Get last commit date and author + COMMIT_DATE=$(gh api "repos/$REPO/commits/$SHA" --jq '.commit.author.date') + AUTHOR=$(gh api "repos/$REPO/commits/$SHA" --jq '.commit.author.name') + COMMIT_TS=$(date -d "$COMMIT_DATE" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$COMMIT_DATE" +%s 2>/dev/null) + AGE_DAYS_CALC=$(( (NOW - COMMIT_TS) / 86400 )) + + # Check if branch was merged via PR + if grep -qxF "$BRANCH" /tmp/merged_prs.txt 2>/dev/null; then + # Merged branch — auto-delete + if [ "$DELETED" -lt "$MAX_DELETIONS" ]; then + ENC=$(printf '%s' "$BRANCH" | jq -sRr @uri) + if gh api -X DELETE "repos/$REPO/git/refs/heads/$ENC" 2>/dev/null; then + DELETED=$((DELETED + 1)) + echo "| \`$BRANCH\` | DELETED | merged via PR | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" + else + SKIPPED=$((SKIPPED + 1)) + echo "| \`$BRANCH\` | SKIP | delete failed | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" + fi + else + SKIPPED=$((SKIPPED + 1)) + echo "| \`$BRANCH\` | DEFERRED | max deletions reached | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" + fi + continue + fi + + # Check if stale (no commits in N days) + if [ "$AGE_DAYS_CALC" -ge "$AGE_DAYS" ]; then + # Stale branch — create issue + if [ "$DELETED" -lt "$MAX_DELETIONS" ]; then + ISSUE_TITLE="Stale branch: \`$BRANCH\` (last commit $AGE_DAYS_CALC days ago)" + ISSUE_BODY="Branch \`$BRANCH\` has no open PR and no commits for $AGE_DAYS_CALC days (since $COMMIT_DATE).\n\nAuthor: $AUTHOR\n\nThis branch was automatically flagged for cleanup. If this branch is still needed, please comment within 7 days. Otherwise, it will be deleted in the next cleanup run." + gh issue create --repo "$REPO" --title "$ISSUE_TITLE" --body "$ISSUE_BODY" --label "stale-branch" 2>/dev/null && ISSUED=$((ISSUED + 1)) + SKIPPED=$((SKIPPED + 1)) + echo "| \`$BRANCH\` | ISSUE CREATED | stale ($AGE_DAYS_CALC days) | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" + else + SKIPPED=$((SKIPPED + 1)) + echo "| \`$BRANCH\` | DEFERRED | max deletions reached | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" + fi + continue + fi + + # Active branch — skip + SKIPPED=$((SKIPPED + 1)) + echo "| \`$BRANCH\` | SKIP | active ($AGE_DAYS_CALC days) | $COMMIT_DATE | $AUTHOR |" >> "$GITHUB_STEP_SUMMARY" + + done < /tmp/branches.txt + + { + echo "" + echo "**Summary:** $DELETED deleted, $ISSUED issues created, $SKIPPED skipped" + echo "" + echo "Config: age_days=$AGE_DAYS, max_deletions=$MAX_DELETIONS" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99319a7..30de8ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,36 +22,36 @@ jobs: steps: - uses: actions/checkout@v7 with: - fetch-depth: 0 + fetch-depth: 0 - name: Check for locked file modifications run: | - # Lê os arquivos bloqueados dinamicamente do CODEOWNERS - LOCKED_FILES=$(awk '!/^#/ && !/^$/ {print $1}' .github/CODEOWNERS | sed 's|^/||') - CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }} HEAD) - - for file in $CHANGED_FILES; do - for locked in $LOCKED_FILES; do - match=false - case "$locked" in - */) - dir="${locked%/}" - case "$file" in - "$dir" | "$dir"/*) match=true ;; - esac ;; - *\*) - # shellcheck disable=SC2254 - case "$file" in - $locked) match=true ;; - esac ;; - *) - [ "$file" = "$locked" ] && match=true ;; - esac - if $match; then - echo "::error file=$file::❌ ERROR: This file ($file) can only be modified in the central repository: victormgomes/laravel-libs-shared" - exit 1 - fi + # Lê os arquivos bloqueados dinamicamente do CODEOWNERS + LOCKED_FILES=$(awk '!/^#/ && !/^$/ {print $1}' .github/CODEOWNERS | sed 's|^/||') + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }} HEAD) + + for file in $CHANGED_FILES; do + for locked in $LOCKED_FILES; do + match=false + case "$locked" in + */) + dir="${locked%/}" + case "$file" in + "$dir" | "$dir"/*) match=true ;; + esac ;; + *\*) + # shellcheck disable=SC2254 + case "$file" in + $locked) match=true ;; + esac ;; + *) + [ "$file" = "$locked" ] && match=true ;; + esac + if $match; then + echo "::error file=$file::❌ ERROR: This file ($file) can only be modified in the central repository: victormgomes/laravel-libs-shared" + exit 1 + fi + done done - done code-quality: uses: victormgomes/laravel-libs-shared/.github/workflows/php-code-quality.yml@main diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 87df74e..80815a5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,21 +1,68 @@ name: Deploy Documentation on: - push: - branches: - - main - paths: - - 'docs/**' - - '.github/workflows/docs.yml' - workflow_dispatch: + push: + branches: + - main + paths: + - "docs/**" + - ".github/workflows/docs.yml" + workflow_dispatch: +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: - contents: read - pages: write - id-token: write + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true jobs: - docs: - uses: victormgomes/laravel-libs-shared/.github/workflows/vitepress-docs.yml@main - with: - docs_dir: 'docs' + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: 20 + + - name: Install VitePress + run: | + if [ ! -f "package.json" ]; then + npm init -y + fi + npm install -D vitepress vue + + - name: Setup Pages + if: ${{ env.ACT != 'true' }} + uses: actions/configure-pages@v6 + + - name: Build with VitePress + run: npx vitepress build docs + + - name: Upload artifact + if: ${{ env.ACT != 'true' }} + uses: actions/upload-pages-artifact@v5 + with: + path: docs/.vitepress/dist + + deploy: + needs: build + runs-on: ubuntu-latest + name: Deploy + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + if: ${{ env.ACT != 'true' }} + id: deployment + uses: actions/deploy-pages@v5 diff --git a/Dockerfile b/Dockerfile index df26fe2..171ba6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,11 +13,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ git \ curl \ gnupg \ - nodejs \ - npm \ && docker-php-ext-install zip \ && rm -rf /var/lib/apt/lists/* +# hadolint ignore=DL3008 +RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && npm install -g npm@latest \ + && rm -rf /var/lib/apt/lists/* + # Install Docker CLI and Sudo (for Docker-out-of-Docker) # hadolint ignore=DL3008 RUN install -m 0755 -d /etc/apt/keyrings \