contributor automation #37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: contributor automation | |
| on: | |
| schedule: | |
| - cron: '0 0 * * *' | |
| pull_request_target: | |
| types: [closed] | |
| branches: | |
| - main | |
| # Prevent multiple runs from overlapping and causing commit conflicts | |
| concurrency: | |
| group: contributor-automation | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write | |
| jobs: | |
| update-readme: | |
| # Security: pull_request_target runs in the context of the base repo. | |
| # We only run if the event is a schedule or a merged PR. | |
| if: github.event_name == 'schedule' || (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout Base Repository | |
| uses: actions/checkout@v4 | |
| with: | |
| # Security: Explicitly checkout the base repository's default branch. | |
| # This ensures we are running trusted code from the main repo, not the fork. | |
| ref: ${{ github.event.repository.default_branch }} | |
| fetch-depth: 0 | |
| - name: Fetch Contributors (Paginated, Sorted by Contributions Descending) | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| echo "[]" > contributors.json | |
| PAGE=1 | |
| echo "Fetching contributors from GitHub API..." | |
| while true; do | |
| # GitHub API returns contributors sorted by contribution count descending by default. | |
| # We preserve this order across pages by appending pages in sequence. | |
| timeout 30s gh api "/repos/${{ github.repository }}/contributors?per_page=100&page=${PAGE}" > "response_${PAGE}.json" || { | |
| echo "API call failed or timed out. Retrying once..." | |
| sleep 5 | |
| timeout 60s gh api "/repos/${{ github.repository }}/contributors?per_page=100&page=${PAGE}" > "response_${PAGE}.json" | |
| } | |
| COUNT=$(jq '. | length' "response_${PAGE}.json") | |
| if [ "$COUNT" -eq 0 ]; then break; fi | |
| # Append page results, filtering out bots — preserve order (no sort) | |
| jq -s '.[0] + [.[1][] | select(.type != "Bot")]' contributors.json "response_${PAGE}.json" > tmp.json && mv tmp.json contributors.json | |
| if [ "$COUNT" -lt 100 ]; then break; fi | |
| PAGE=$((PAGE + 1)) | |
| done | |
| # Extract logins in contribution-count order (API order, descending). | |
| # Do NOT use sort — that would override the descending contribution order. | |
| jq -r '.[] | .login' contributors.json > contributor_logins.txt | |
| echo "Contributors fetched (top to bottom = most to fewest contributions):" | |
| cat contributor_logins.txt | |
| - name: Build Professional Contributor Grid | |
| run: | | |
| set -euo pipefail | |
| # UI: Professional 6-column HTML grid with avatars, ordered by contribution count (desc) | |
| echo "<table border=\"0\">" > gallery.html | |
| echo " <tr>" >> gallery.html | |
| COL=0 | |
| while IFS= read -r USERNAME; do | |
| if [ "$COL" -eq 6 ]; then | |
| echo " </tr>" >> gallery.html | |
| echo " <tr>" >> gallery.html | |
| COL=0 | |
| fi | |
| echo " <td align=\"center\" width=\"120\">" >> gallery.html | |
| echo " <a href=\"https://github.com/${USERNAME}\">" >> gallery.html | |
| echo " <img src=\"https://github.com/${USERNAME}.png?size=100\" width=\"100\" height=\"100\" style=\"border-radius:50%; border:2px solid #555;\" alt=\"${USERNAME}\" /><br />" >> gallery.html | |
| echo " <sub><b>@${USERNAME}</b></sub>" >> gallery.html | |
| echo " </a>" >> gallery.html | |
| echo " </td>" >> gallery.html | |
| COL=$((COL + 1)) | |
| done < contributor_logins.txt | |
| # Fill remaining empty cells to maintain grid structure | |
| while [ "$COL" -lt 6 ] && [ "$COL" -gt 0 ]; do | |
| echo " <td width=\"120\"></td>" >> gallery.html | |
| COL=$((COL + 1)) | |
| done | |
| echo " </tr>" >> gallery.html | |
| echo "</table>" >> gallery.html | |
| - name: Update README.md | |
| run: | | |
| set -euo pipefail | |
| # Verify markers exist before modification | |
| if ! grep -q "<!-- CONTRIBUTORS_START -->" README.md || ! grep -q "<!-- CONTRIBUTORS_END -->" README.md; then | |
| echo "Error: Markers missing in README.md. Add these two HTML comments to README.md:" | |
| echo " <!-- CONTRIBUTORS_START -->" | |
| echo " <!-- CONTRIBUTORS_END -->" | |
| exit 1 | |
| fi | |
| # Surgical replacement using markers | |
| sed -n '1,/<!-- CONTRIBUTORS_START -->/p' README.md > README.new | |
| cat gallery.html >> README.new | |
| sed -n '/<!-- CONTRIBUTORS_END -->/,$p' README.md >> README.new | |
| mv README.new README.md | |
| - name: Commit and Push | |
| run: | | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| git add README.md | |
| if git diff --staged --quiet; then | |
| echo "No changes to README." | |
| else | |
| git commit -m "docs: update contributor gallery [skip ci]" | |
| git push origin HEAD:${{ github.event.repository.default_branch }} | |
| fi |