Skip to content

Auto Stargazer Outreach #18

Auto Stargazer Outreach

Auto Stargazer Outreach #18

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