Auto Stargazer Outreach #59
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: Auto Stargazer Outreach | |
| on: | |
| schedule: | |
| - cron: "20 2 * * *" | |
| workflow_dispatch: | |
| inputs: | |
| discussion_number: | |
| description: "Discussion number to post invitations" | |
| required: true | |
| default: "54" | |
| type: string | |
| language: | |
| description: "Invitation language" | |
| required: true | |
| default: "zh" | |
| type: choice | |
| options: | |
| - zh | |
| - en | |
| max_candidates: | |
| description: "Max users to process per run" | |
| required: true | |
| default: "3" | |
| type: string | |
| publish_comment: | |
| description: "Post generated invitation comments" | |
| required: true | |
| default: true | |
| type: boolean | |
| org_name: | |
| description: "Organization for shared-membership + contribution checks" | |
| required: false | |
| default: "TashanGKD" | |
| type: string | |
| permissions: | |
| contents: read | |
| discussions: write | |
| jobs: | |
| discover: | |
| # 默认禁用:仅当仓库变量 ENABLE_STARGAZER_OUTREACH=true 时执行 | |
| if: vars.ENABLE_STARGAZER_OUTREACH == 'true' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has_candidates: ${{ steps.pick.outputs.has_candidates }} | |
| matrix_json: ${{ steps.pick.outputs.matrix_json }} | |
| candidate_count: ${{ steps.pick.outputs.candidate_count }} | |
| env: | |
| GH_TOKEN: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }} | |
| DISCUSSION_NUMBER: ${{ github.event.inputs.discussion_number || '54' }} | |
| MAX_CANDIDATES: ${{ github.event.inputs.max_candidates || '3' }} | |
| REPO: ${{ github.repository }} | |
| REPO_OWNER: ${{ github.repository_owner }} | |
| steps: | |
| - name: Collect stargazers/forks/discussion comments | |
| run: | | |
| set -euo pipefail | |
| mkdir -p artifacts | |
| gh api "repos/${REPO}/stargazers" --paginate -H "Accept: application/vnd.github.star+json" > artifacts/stargazers_raw.json | |
| jq '[.[] | {login: .user.login, starred_at}] | unique_by(.login)' artifacts/stargazers_raw.json > artifacts/stargazers.json | |
| gh api "repos/${REPO}/forks" --paginate --jq '.[].owner.login' | sort -u > artifacts/fork_owners.txt | |
| if gh api "repos/${REPO}/discussions/${DISCUSSION_NUMBER}/comments" --paginate --jq '.[].body' > artifacts/discussion_comments.txt; then | |
| true | |
| else | |
| : > artifacts/discussion_comments.txt | |
| fi | |
| grep -oE '<!-- issuelab-outreach:[a-zA-Z0-9-]+ -->' artifacts/discussion_comments.txt \ | |
| | sed -E 's/.*issuelab-outreach:([a-zA-Z0-9-]+).*/\1/' \ | |
| | sort -u > artifacts/already_invited.txt || true | |
| : > artifacts/candidates.tsv | |
| while IFS=$'\t' read -r login starred_at; do | |
| [ -n "${login}" ] || continue | |
| [ "${login}" != "${REPO_OWNER}" ] || continue | |
| grep -qx "${login}" artifacts/fork_owners.txt && continue | |
| grep -qx "${login}" artifacts/already_invited.txt && continue | |
| printf "%s\t%s\n" "${login}" "${starred_at}" >> artifacts/candidates.tsv | |
| done < <(jq -r '.[] | [.login, .starred_at] | @tsv' artifacts/stargazers.json) | |
| - name: Pick candidate matrix | |
| id: pick | |
| run: | | |
| set -euo pipefail | |
| if [ ! -s artifacts/candidates.tsv ]; then | |
| echo "has_candidates=false" >> "$GITHUB_OUTPUT" | |
| echo "candidate_count=0" >> "$GITHUB_OUTPUT" | |
| echo 'matrix_json=[]' >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| head -n "${MAX_CANDIDATES}" artifacts/candidates.tsv > artifacts/selected.tsv | |
| CANDIDATE_COUNT=$(wc -l < artifacts/selected.tsv | tr -d ' ') | |
| MATRIX_JSON=$(jq -R -s -c ' | |
| split("\n") | |
| | map(select(length > 0)) | |
| | map(split("\t")) | |
| | map({target_username: .[0], starred_at: .[1]}) | |
| ' artifacts/selected.tsv) | |
| echo "has_candidates=true" >> "$GITHUB_OUTPUT" | |
| echo "candidate_count=${CANDIDATE_COUNT}" >> "$GITHUB_OUTPUT" | |
| echo "matrix_json=${MATRIX_JSON}" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "## Stargazer Outreach Discovery" | |
| echo "- repo: ${REPO}" | |
| echo "- discussion: #${DISCUSSION_NUMBER}" | |
| echo "- stargazers: $(jq 'length' artifacts/stargazers.json)" | |
| echo "- fork owners: $(wc -l < artifacts/fork_owners.txt | tr -d ' ')" | |
| echo "- already invited (marker): $(wc -l < artifacts/already_invited.txt 2>/dev/null || echo 0)" | |
| echo "- selected candidates: ${CANDIDATE_COUNT}" | |
| echo | |
| echo "### Selected" | |
| awk -F'\t' '{printf("- @%s (starred_at: %s)\n",$1,$2)}' artifacts/selected.tsv | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload discovery artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: stargazer-discovery-${{ github.run_id }} | |
| path: artifacts/ | |
| retention-days: 7 | |
| outreach: | |
| needs: discover | |
| if: needs.discover.outputs.has_candidates == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 2 | |
| matrix: | |
| candidate: ${{ fromJson(needs.discover.outputs.matrix_json) }} | |
| env: | |
| TARGET_USERNAME: ${{ matrix.candidate.target_username }} | |
| STARRED_AT: ${{ matrix.candidate.starred_at }} | |
| LANGUAGE: ${{ github.event.inputs.language || 'zh' }} | |
| DISCUSSION_NUMBER: ${{ github.event.inputs.discussion_number || '54' }} | |
| ORG_NAME: ${{ github.event.inputs.org_name || 'TashanGKD' }} | |
| PUBLISH_COMMENT: ${{ github.event.inputs.publish_comment || 'true' }} | |
| GH_TOKEN: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }} | |
| ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }} | |
| ANTHROPIC_MODEL: ${{ secrets.ANTHROPIC_MODEL }} | |
| REPO_OWNER: ${{ github.repository_owner }} | |
| REPO_NAME: ${{ github.event.repository.name }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Collect user signals | |
| run: | | |
| set -euo pipefail | |
| mkdir -p artifacts | |
| gh api "users/${TARGET_USERNAME}" > artifacts/user.json | |
| gh api "users/${TARGET_USERNAME}/repos?per_page=100" > artifacts/repos.json || echo "[]" > artifacts/repos.json | |
| gh api "users/${TARGET_USERNAME}/events/public?per_page=100" > artifacts/events.json || echo "[]" > artifacts/events.json | |
| gh api "users/${TARGET_USERNAME}/starred?per_page=100" > artifacts/starred.json || echo "[]" > artifacts/starred.json | |
| gh api "users/${TARGET_USERNAME}/following?per_page=100" > artifacts/following.json || echo "[]" > artifacts/following.json | |
| gh api "users/${TARGET_USERNAME}/orgs?per_page=100" > artifacts/user_orgs.json || echo "[]" > artifacts/user_orgs.json | |
| gh api "orgs/${ORG_NAME}/members?per_page=100" --paginate --jq '.[].login' > artifacts/org_members.txt || true | |
| if grep -qx "${TARGET_USERNAME}" artifacts/org_members.txt 2>/dev/null; then | |
| IN_ORG=true | |
| else | |
| IN_ORG=false | |
| fi | |
| echo "IN_ORG=${IN_ORG}" >> "$GITHUB_ENV" | |
| gh api "orgs/${ORG_NAME}/repos?per_page=100" --paginate --jq '.[].name' > artifacts/org_repos.txt || true | |
| : > artifacts/org_contrib.tsv | |
| while IFS= read -r repo; do | |
| [ -n "${repo}" ] || continue | |
| count=$(gh api "repos/${ORG_NAME}/${repo}/commits?author=${TARGET_USERNAME}&per_page=100" --jq 'length' 2>/dev/null || echo 0) | |
| if [ "${count}" -gt 0 ]; then | |
| latest=$(gh api "repos/${ORG_NAME}/${repo}/commits?author=${TARGET_USERNAME}&per_page=1" --jq '.[0].commit.author.date' 2>/dev/null || echo "") | |
| msg=$(gh api "repos/${ORG_NAME}/${repo}/commits?author=${TARGET_USERNAME}&per_page=1" --jq '.[0].commit.message' 2>/dev/null | head -n 1 | tr '\t' ' ') | |
| echo -e "${repo}\t${count}\t${latest}\t${msg}" >> artifacts/org_contrib.tsv | |
| fi | |
| done < artifacts/org_repos.txt | |
| { | |
| echo "# Target" | |
| echo "- username: ${TARGET_USERNAME}" | |
| echo "- starred_at: ${STARRED_AT}" | |
| echo "- language: ${LANGUAGE}" | |
| echo "- org_name: ${ORG_NAME}" | |
| echo "- in_org: ${IN_ORG}" | |
| echo | |
| echo "# Profile" | |
| jq -r '"- login: \(.login)\n- name: \(.name // "null")\n- company: \(.company // "null")\n- location: \(.location // "null")\n- bio: \(.bio // "null")\n- public_repos: \(.public_repos)\n- followers: \(.followers)\n- following: \(.following)\n- created_at: \(.created_at)\n- updated_at: \(.updated_at)"' artifacts/user.json | |
| echo | |
| echo "# Recent repos (top 10 by updated_at)" | |
| jq -r 'sort_by(.updated_at) | reverse | .[:10][] | "- \(.name) | lang=\(.language // "null") | stars=\(.stargazers_count) | updated=\(.updated_at)"' artifacts/repos.json | |
| echo | |
| echo "# Recent events (latest 20)" | |
| jq -r '.[:20][] | "- \(.type) | \(.repo.name) | \(.created_at)"' artifacts/events.json | |
| echo | |
| echo "# Starred repos (latest 30)" | |
| jq -r '.[:30][] | "- \(.full_name)"' artifacts/starred.json | |
| echo | |
| echo "# Following (first 30)" | |
| jq -r '.[:30][] | "- \(.login)"' artifacts/following.json | |
| echo | |
| echo "# Org contributions in ${ORG_NAME}" | |
| if [ -s artifacts/org_contrib.tsv ]; then | |
| awk -F '\t' '{printf("- %s | commits=%s | latest=%s | msg=%s\n",$1,$2,$3,$4)}' artifacts/org_contrib.tsv | |
| else | |
| echo "- no commit evidence found via author=${TARGET_USERNAME}" | |
| fi | |
| } > artifacts/context.md | |
| - name: Validate model/provider config | |
| env: | |
| ANTHROPIC_AUTH_TOKEN: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| [ -n "${ANTHROPIC_AUTH_TOKEN:-}" ] || { echo "Missing ANTHROPIC_AUTH_TOKEN"; exit 1; } | |
| [ -n "${ANTHROPIC_BASE_URL:-}" ] || { echo "Missing ANTHROPIC_BASE_URL"; exit 1; } | |
| [ -n "${ANTHROPIC_MODEL:-}" ] || { echo "Missing ANTHROPIC_MODEL"; exit 1; } | |
| - name: Run Claude for analysis + invitation | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| github_token: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }} | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} | |
| claude_args: | | |
| --model ${{ env.ANTHROPIC_MODEL }} | |
| --allowedTools Write,Bash(ls),Bash(cat),Bash(test),Bash(echo),Bash(mkdir) | |
| prompt: | | |
| Read: | |
| - .github/prompts/stargazer_outreach.md | |
| - artifacts/context.md | |
| - artifacts/user.json | |
| - artifacts/repos.json | |
| - artifacts/events.json | |
| - artifacts/starred.json | |
| - artifacts/org_contrib.tsv | |
| Then strictly follow the prompt spec and write: | |
| - artifacts/analysis.md | |
| - artifacts/invitation.md | |
| Variables: | |
| - TARGET_USERNAME=${{ env.TARGET_USERNAME }} | |
| - LANGUAGE=${{ env.LANGUAGE }} | |
| - IN_ORG=${{ env.IN_ORG }} | |
| - ORG_NAME=${{ env.ORG_NAME }} | |
| - name: Verify generated outputs | |
| run: | | |
| set -euo pipefail | |
| [ -s artifacts/analysis.md ] || { echo "Missing artifacts/analysis.md"; exit 1; } | |
| [ -s artifacts/invitation.md ] || { echo "Missing artifacts/invitation.md"; exit 1; } | |
| - name: Post invitation to discussion | |
| if: ${{ github.event.inputs.publish_comment != 'false' }} | |
| run: | | |
| set -euo pipefail | |
| if [ ! -s artifacts/invitation.md ]; then | |
| echo "Skip posting: artifacts/invitation.md not found" | |
| exit 0 | |
| fi | |
| DISC_ID=$(gh api graphql -f query='query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){discussion(number:$number){id}}}' -f owner="${REPO_OWNER}" -f name="${REPO_NAME}" -F number="${DISCUSSION_NUMBER}" --jq '.data.repository.discussion.id') | |
| BODY="<!-- issuelab-outreach:${TARGET_USERNAME} -->"$'\n'"$(cat artifacts/invitation.md)" | |
| gh api graphql \ | |
| -f query='mutation($discussionId:ID!,$body:String!){addDiscussionComment(input:{discussionId:$discussionId,body:$body}){comment{url}}}' \ | |
| -f discussionId="${DISC_ID}" \ | |
| -f body="${BODY}" | |
| - name: Add run summary | |
| run: | | |
| { | |
| echo "## Outreach Result" | |
| echo "- target: @${TARGET_USERNAME}" | |
| echo "- starred_at: ${STARRED_AT}" | |
| echo "- discussion: #${DISCUSSION_NUMBER}" | |
| echo "- published: ${{ github.event.inputs.publish_comment != 'false' }}" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload candidate artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: stargazer-outreach-${{ github.run_id }}-${{ matrix.candidate.target_username }} | |
| path: artifacts/ | |
| retention-days: 7 |