|
| 1 | +name: "Ambassador Accepted: Post + Export" |
| 2 | + |
| 3 | +on: |
| 4 | + workflow_dispatch: |
| 5 | + inputs: |
| 6 | + label_name: |
| 7 | + description: "Target label" |
| 8 | + required: true |
| 9 | + default: Accepted |
| 10 | + type: string |
| 11 | + dry_run: |
| 12 | + description: "Preview without posting" |
| 13 | + required: true |
| 14 | + default: false |
| 15 | + type: boolean |
| 16 | + |
| 17 | +permissions: |
| 18 | + contents: read |
| 19 | + issues: write |
| 20 | + |
| 21 | +jobs: |
| 22 | + post-and-export: |
| 23 | + runs-on: ubuntu-latest |
| 24 | + steps: |
| 25 | + - name: Checkout repo |
| 26 | + uses: actions/checkout@v4 |
| 27 | + |
| 28 | + - name: Set up Python |
| 29 | + uses: actions/setup-python@v5 |
| 30 | + with: |
| 31 | + python-version: "3.11" |
| 32 | + |
| 33 | + - name: Install deps |
| 34 | + run: pip install requests |
| 35 | + |
| 36 | + - name: Post message and export CSV |
| 37 | + env: |
| 38 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 39 | + REPO_FULL: ${{ github.repository }} |
| 40 | + LABEL_NAME: ${{ inputs.label_name }} |
| 41 | + DRY_RUN: ${{ inputs.dry_run }} |
| 42 | + run: | |
| 43 | + python - <<'PY' |
| 44 | + import os, re, csv, sys, requests |
| 45 | + from requests.adapters import HTTPAdapter, Retry |
| 46 | +
|
| 47 | + token = os.getenv("GITHUB_TOKEN") |
| 48 | + repo_full = os.getenv("REPO_FULL") |
| 49 | + label_name = os.getenv("LABEL_NAME","Accepted") |
| 50 | + dry_run = (os.getenv("DRY_RUN","false").lower()=="true") |
| 51 | +
|
| 52 | + if not token or not repo_full: |
| 53 | + print("Missing GITHUB_TOKEN or REPO_FULL", file=sys.stderr); sys.exit(1) |
| 54 | + owner, repo = repo_full.split("/",1) |
| 55 | +
|
| 56 | + s = requests.Session() |
| 57 | + s.headers.update({ |
| 58 | + "Authorization": f"Bearer {token}", |
| 59 | + "Accept": "application/vnd.github+json", |
| 60 | + "X-GitHub-Api-Version": "2022-11-28" |
| 61 | + }) |
| 62 | + s.mount("https://", HTTPAdapter(max_retries=Retry( |
| 63 | + total=8, backoff_factor=0.7, |
| 64 | + status_forcelist=[429,500,502,503,504], |
| 65 | + allowed_methods=["GET","POST","HEAD","OPTIONS"] |
| 66 | + ))) |
| 67 | +
|
| 68 | + MESSAGE_TEMPLATE = """Hello {first_name} |
| 69 | +
|
| 70 | +You’ve been invited to join the private **Ambassador Program Management Repository**: |
| 71 | +👉 [pytorch-fdn/ambassador-program-management](https://github.com/pytorch-fdn/ambassador-program-management/) |
| 72 | + |
| 73 | +Once you accept the invitation, you’ll find important information in the discussion thread — including instructions to submit a new issue using the **“Ambassador Confirmation & Details”** template. |
| 74 | + |
| 75 | +To confirm your participation in the 2025 PyTorch Ambassador Program, please open a new issue using the **“Ambassador Confirmation & Details”** template here: |
| 76 | +👉 [Create your confirmation issue](https://github.com/pytorch-fdn/ambassador-program-management/issues/new/choose) |
| 77 | + |
| 78 | +_Completing this step will confirm your role and get you ready for onboarding 🚀_ |
| 79 | + |
| 80 | +As part of onboarding, we’ll be hosting a **Kick-Off Call** where we’ll share important program details, expectations, and next steps: |
| 81 | + |
| 82 | +📅 **Date:** Monday, September 8 |
| 83 | +🕒 **Time:** 5:30 PM CEST / 8:30 AM PDT |
| 84 | + |
| 85 | +Once you confirm your role, you will also receive a calendar invite with the meeting link. |
| 86 | + |
| 87 | +We’re truly excited to welcome you into the PyTorch Ambassador Program and look forward to the impact you’ll continue to make in the community. |
| 88 | + |
| 89 | +Best regards, |
| 90 | +The PyTorch PMO |
| 91 | +""" |
| 92 | +
|
| 93 | + # ---------- Helpers ---------- |
| 94 | + def get_accepted_open_issues(): |
| 95 | + url = f"https://api.github.com/repos/{owner}/{repo}/issues" |
| 96 | + params = {"state":"open", "labels":label_name, "per_page":100} |
| 97 | + while url: |
| 98 | + r = s.get(url, params=params if 'params' in locals() else None) |
| 99 | + if r.status_code != 200: |
| 100 | + print(f"Failed to list issues: {r.status_code} {r.text}", file=sys.stderr); sys.exit(1) |
| 101 | + for it in r.json(): |
| 102 | + if "pull_request" not in it: |
| 103 | + yield it |
| 104 | + url = r.links.get("next",{}).get("url") |
| 105 | + params = None |
| 106 | + |
| 107 | + def parse_after_label(body:str, label:str): |
| 108 | + """ |
| 109 | + Finds the line immediately following a label header block: |
| 110 | + Label |
| 111 | + value |
| 112 | + Returns '' if not found. |
| 113 | + """ |
| 114 | + pat = rf"{re.escape(label)}\s*\r?\n([^\n\r]+)" |
| 115 | + m = re.search(pat, body, re.IGNORECASE) |
| 116 | + return m.group(1).strip() if m else "" |
| 117 | + |
| 118 | + def extract_fields(issue): |
| 119 | + body = issue.get("body") or "" |
| 120 | + nominee_name = parse_after_label(body, "Nominee Name") |
| 121 | + nominee_email = parse_after_label(body, "Nominee Email") |
| 122 | + location = parse_after_label(body, "City, State/Province, Country") |
| 123 | + gh_handle = parse_after_label(body, "Nominee's GitHub or GitLab Handle") |
| 124 | + |
| 125 | + # Normalize handle: if it's a URL like https://github.com/user -> take last path segment |
| 126 | + if gh_handle.startswith("http"): |
| 127 | + m = re.search(r"github\.com/([^/\s]+)", gh_handle, re.IGNORECASE) |
| 128 | + if m: |
| 129 | + gh_handle = m.group(1) |
| 130 | + |
| 131 | + # First name: from nominee_name first token; fallback to profile name; then login |
| 132 | + first_name = "" |
| 133 | + if nominee_name: |
| 134 | + first_name = nominee_name.split()[0].strip() |
| 135 | + if not first_name: |
| 136 | + user = issue.get("user") or {} |
| 137 | + prof_name = (user.get("name") or "").strip() |
| 138 | + if prof_name: |
| 139 | + first_name = prof_name.split()[0] |
| 140 | + else: |
| 141 | + first_name = (user.get("login") or "there") |
| 142 | + |
| 143 | + return { |
| 144 | + "issue_number": issue["number"], |
| 145 | + "first_name": first_name, |
| 146 | + "nominee_name": nominee_name, |
| 147 | + "nominee_email": nominee_email, |
| 148 | + "location": location, |
| 149 | + "github_handle": gh_handle |
| 150 | + } |
| 151 | + |
| 152 | + def post_comment(issue_number, body): |
| 153 | + if dry_run: |
| 154 | + print(f"DRY-RUN: would post to #{issue_number}:\n{body[:300]}...\n---") |
| 155 | + return |
| 156 | + r = s.post( |
| 157 | + f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}/comments", |
| 158 | + json={"body": body} |
| 159 | + ) |
| 160 | + if r.status_code not in (200,201): |
| 161 | + print(f"Failed to comment on #{issue_number}: {r.status_code} {r.text}", file=sys.stderr) |
| 162 | + |
| 163 | + # ---------- Process ---------- |
| 164 | + rows = [] |
| 165 | + count = 0 |
| 166 | + for issue in get_accepted_open_issues(): |
| 167 | + data = extract_fields(issue) |
| 168 | + msg = MESSAGE_TEMPLATE.format(first_name=data["first_name"]) |
| 169 | + post_comment(data["issue_number"], msg) |
| 170 | + count += 1 |
| 171 | + rows.append([ |
| 172 | + data["issue_number"], |
| 173 | + data["nominee_name"], |
| 174 | + data["nominee_email"], |
| 175 | + data["location"], |
| 176 | + data["github_handle"] |
| 177 | + ]) |
| 178 | + |
| 179 | + # Export CSV |
| 180 | + out = "accepted_export.csv" |
| 181 | + with open(out, "w", newline="", encoding="utf-8") as f: |
| 182 | + w = csv.writer(f) |
| 183 | + w.writerow(["Issue", "Nominee Name", "Nominee Email", "Location", "GitHub Handle"]) |
| 184 | + w.writerows(rows) |
| 185 | + |
| 186 | + print(f"Posted to {count} accepted issue(s).") |
| 187 | + print(f"Exported {len(rows)} row(s) -> {out}") |
| 188 | + PY |
| 189 | + |
| 190 | + - name: Upload export artifact |
| 191 | + uses: actions/upload-artifact@v4 |
| 192 | + with: |
| 193 | + name: accepted_export |
| 194 | + path: accepted_export.csv |
0 commit comments