Skip to content

Commit 7cafd01

Browse files
Update amb-comments.yml
1 parent e74bd36 commit 7cafd01

File tree

1 file changed

+145
-201
lines changed

1 file changed

+145
-201
lines changed

.github/workflows/amb-comments.yml

Lines changed: 145 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,13 @@
1-
name: "Ambassador comments (decisions only)"
1+
name: "Ambassador Accepted: Post + Export"
22

33
on:
44
workflow_dispatch:
55
inputs:
6-
private_repo:
7-
description: "Private repo containing the CSVs (owner/name)"
6+
label_name:
7+
description: "Target label"
88
required: true
9-
default: pytorch-fdn/ambassador-program-management
9+
default: Accepted
1010
type: string
11-
private_ref:
12-
description: "Branch/tag/sha of the private repo"
13-
required: true
14-
default: main
15-
type: string
16-
decision_csv_path:
17-
description: "Path to decision CSV (Submission ID, Decision, Candidate)"
18-
required: true
19-
default: .github/ISSUE_TEMPLATE/dmsap.csv
20-
type: string
21-
unlock_locked_issues:
22-
description: "Temporarily unlock locked issues to post, then re-lock"
23-
required: true
24-
default: true
25-
type: boolean
2611
dry_run:
2712
description: "Preview without posting"
2813
required: true
@@ -33,218 +18,177 @@ permissions:
3318
contents: read
3419
issues: write
3520

36-
concurrency:
37-
group: ambassador-comments
38-
cancel-in-progress: true
39-
4021
jobs:
41-
comment:
22+
post-and-export:
4223
runs-on: ubuntu-latest
4324
steps:
44-
- name: Checkout PUBLIC repo
25+
- name: Checkout repo
4526
uses: actions/checkout@v4
4627

47-
- name: Checkout PRIVATE repo
48-
uses: actions/checkout@v4
49-
with:
50-
repository: ${{ inputs.private_repo }}
51-
ref: ${{ inputs.private_ref }}
52-
token: ${{ secrets.PRIVATE_REPO_TOKEN }}
53-
path: private-src
54-
5528
- name: Set up Python
5629
uses: actions/setup-python@v5
5730
with:
5831
python-version: "3.11"
5932

60-
- name: Install dependencies
33+
- name: Install deps
6134
run: pip install requests
6235

63-
- name: Post comments from decision.csv
36+
- name: Post message and export CSV
6437
env:
6538
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6639
REPO_FULL: ${{ github.repository }}
67-
DEC_CSV_ABS: ${{ github.workspace }}/private-src/${{ inputs.decision_csv_path }}
68-
UNLOCK_LOCKED: ${{ inputs.unlock_locked_issues }}
40+
LABEL_NAME: ${{ inputs.label_name }}
6941
DRY_RUN: ${{ inputs.dry_run }}
7042
run: |
7143
python - <<'PY'
72-
import os, csv, re, sys, urllib.parse
73-
import requests
44+
import os, re, csv, sys, requests
7445
from requests.adapters import HTTPAdapter, Retry
7546
7647
token = os.getenv("GITHUB_TOKEN")
7748
repo_full = os.getenv("REPO_FULL")
78-
dec_csv = os.getenv("DEC_CSV_ABS")
79-
dry_run = (os.getenv("DRY_RUN","false").lower() == "true")
80-
UNLOCK_LOCKED = (os.getenv("UNLOCK_LOCKED","false").lower() == "true")
49+
label_name = os.getenv("LABEL_NAME","Accepted")
50+
dry_run = (os.getenv("DRY_RUN","false").lower()=="true")
8151
8252
if not token or not repo_full:
8353
print("Missing GITHUB_TOKEN or REPO_FULL", file=sys.stderr); sys.exit(1)
54+
owner, repo = repo_full.split("/",1)
8455
85-
owner, repo = repo_full.split("/", 1)
86-
87-
# session with retries
8856
s = requests.Session()
89-
retries = Retry(total=8, backoff_factor=0.7,
90-
status_forcelist=[429,500,502,503,504],
91-
allowed_methods=["GET","POST","PATCH","DELETE","HEAD","OPTIONS"])
92-
s.mount("https://", HTTPAdapter(max_retries=retries))
93-
s.headers.update({"Authorization": f"Bearer {token}",
94-
"Accept": "application/vnd.github+json",
95-
"X-GitHub-Api-Version": "2022-11-28"})
96-
97-
def norm_key(x): return re.sub(r"[^a-z0-9]+","",(x or "").strip().lower())
98-
99-
def get_issue_labels(num):
100-
r = s.get(f"https://api.github.com/repos/{owner}/{repo}/issues/{num}/labels?per_page=100")
101-
if r.status_code==200: return {x["name"] for x in r.json()}
102-
print(f"Warn: get labels #{num} failed: {r.status_code} {r.text}"); return set()
103-
104-
def is_locked(num):
105-
r = s.get(f"https://api.github.com/repos/{owner}/{repo}/issues/{num}")
106-
if r.status_code == 200:
107-
j = r.json()
108-
return bool(j.get("locked")), j.get("active_lock_reason")
109-
print(f"Warn: GET issue #{num} failed: {r.status_code} {r.text}")
110-
return False, None
111-
112-
def unlock_issue(num):
113-
if dry_run:
114-
print(f"DRY-RUN: would UNLOCK issue #{num}")
115-
return True
116-
r = s.delete(f"https://api.github.com/repos/{owner}/{repo}/issues/{num}/lock")
117-
if r.status_code in (200,204):
118-
return True
119-
print(f"Warn: UNLOCK failed for #{num}: {r.status_code} {r.text}")
120-
return False
121-
122-
def relock_issue(num, reason):
123-
if dry_run:
124-
print(f"DRY-RUN: would RE-LOCK issue #{num} (reason={reason or 'none'})")
125-
return True
126-
payload = {"lock_reason": reason} if reason else {}
127-
r = s.put(f"https://api.github.com/repos/{owner}/{repo}/issues/{num}/lock", json=payload)
128-
if r.status_code in (200,204):
129-
return True
130-
print(f"Warn: RE-LOCK failed for #{num}: {r.status_code} {r.text}")
131-
return False
132-
133-
def add_comment_once(num, body, marker):
134-
# 1) avoid duplicate comments using hidden marker
135-
url = f"https://api.github.com/repos/{owner}/{repo}/issues/{num}/comments?per_page=100"
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}
13697
while url:
137-
r = s.get(url)
138-
if r.status_code!=200:
139-
print(f"Warn: list comments #{num} failed: {r.status_code} {r.text}")
140-
break
141-
if any(marker in (c.get("body") or "") for c in r.json()):
142-
print(f"Skip duplicate comment on #{num}")
143-
return
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
144104
url = r.links.get("next",{}).get("url")
145-
146-
# 2) try to comment
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):
147153
if dry_run:
148-
print(f"DRY-RUN: would comment on #{num}:\n{body}\n---")
154+
print(f"DRY-RUN: would post to #{issue_number}:\n{body[:300]}...\n---")
149155
return
150-
r = s.post(f"https://api.github.com/repos/{owner}/{repo}/issues/{num}/comments", json={"body": body})
151-
if r.status_code in (200,201):
152-
return
153-
154-
# 3) if locked and allowed, unlock -> comment -> re-lock
155-
if r.status_code == 403 and "locked" in (r.text or "").lower():
156-
if not UNLOCK_LOCKED:
157-
print(f"Locked: #{num} (skipping; set UNLOCK_LOCKED=true to unlock temporarily).")
158-
return
159-
locked, reason = is_locked(num)
160-
if not locked:
161-
print(f"Comment 403 but issue #{num} not reported locked; skipping.")
162-
return
163-
if unlock_issue(num):
164-
r2 = s.post(f"https://api.github.com/repos/{owner}/{repo}/issues/{num}/comments", json={"body": body})
165-
if r2.status_code not in (200,201):
166-
print(f"Warn: comment after UNLOCK failed for #{num}: {r2.status_code} {r2.text}")
167-
relock_issue(num, reason) # re-lock regardless
168-
return
169-
170-
# 4) other failures
171-
print(f"Warn: comment #{num} failed: {r.status_code} {r.text}")
172-
173-
# messages + markers
174-
ACCEPT_MARKER = "<!-- ambassador-decision:accepted-2025 -->"
175-
REJECT_MARKER = "<!-- ambassador-decision:rejected-2025 -->"
176-
177-
ACCEPT_MSG = (
178-
"Hello {name},\n\n"
179-
"Congratulations! 🎉 We're excited to inform you that you have been selected as a 2025 PyTorch Ambassador. "
180-
"Your contributions, impact, and commitment to the PyTorch community stood out during the review process.\n\n"
181-
"You should also have received an invitation to join our private program management repository, "
182-
"where onboarding details and next steps will be shared. If you do not see the invite, please comment here and we will assist you.\n\n"
183-
"Best regards,\n"
184-
"The PyTorch PMO\n\n" + ACCEPT_MARKER
185-
)
186-
REJECT_MSG = (
187-
"Hello {name},\n\n"
188-
"Thank you for applying to the 2025 PyTorch Ambassador Program and for the valuable contributions you make in the PyTorch community. "
189-
"After careful consideration, we regret to inform you that your application was not selected in this cycle.\n\n"
190-
"We encourage you to apply again in the future — the next application cycle will open in early 2026. "
191-
"Please keep an eye on our website for updates on the timeline: https://pytorch.org/programs/ambassadors/\n\n"
192-
"We truly appreciate the time, effort, and passion you bring to the PyTorch community, and we hope to see your application again in the future.\n\n"
193-
"If you have any questions in the meantime, please feel free to reach out to us at [email protected].\n\n"
194-
"Best regards,\n"
195-
"The PyTorch PMO\n\n" + REJECT_MARKER
196-
)
197-
198-
# ---- Iterate decision.csv rows and post per-row messages with Candidate name ----
199-
counts={"rows":0,"accept_commented":0,"reject_commented":0,"skipped":0}
200-
try:
201-
with open(dec_csv, newline="", encoding="utf-8") as f:
202-
rdr = csv.DictReader(f)
203-
hdr = {norm_key(h): h for h in (rdr.fieldnames or [])}
204-
sid_key = hdr.get("submissionid") or hdr.get("issue") or hdr.get("issuenumber") or hdr.get("id")
205-
decision_key = hdr.get("decision")
206-
name_key = hdr.get("candidate")
207-
if not sid_key or not decision_key or not name_key:
208-
print("decision.csv must contain 'Submission ID', 'Decision', and 'Candidate' columns.", file=sys.stderr); sys.exit(1)
209-
210-
for idx,row in enumerate(rdr, start=2):
211-
counts["rows"] += 1
212-
raw_id = (row.get(sid_key) or "").strip()
213-
decision_raw = (row.get(decision_key) or "").strip().lower()
214-
candidate = (row.get(name_key) or "").strip() or "there"
215-
216-
if not raw_id:
217-
counts["skipped"] += 1; continue
218-
try:
219-
num = int(float(raw_id))
220-
except:
221-
print(f"Row {idx}: invalid Submission ID '{raw_id}'; skipping.")
222-
counts["skipped"] += 1; continue
223-
224-
labels = get_issue_labels(num)
225-
226-
if decision_raw in ("accept","accepted","approve","approved"):
227-
if "Accepted" in labels:
228-
add_comment_once(num, ACCEPT_MSG.format(name=candidate), ACCEPT_MARKER)
229-
counts["accept_commented"] += 1
230-
else:
231-
print(f"Row {idx}: issue #{num} lacks 'Accepted' label; skipping.")
232-
counts["skipped"] += 1
233-
234-
elif decision_raw in ("reject","rejected","decline","declined"):
235-
if "Rejected" in labels:
236-
add_comment_once(num, REJECT_MSG.format(name=candidate), REJECT_MARKER)
237-
counts["reject_commented"] += 1
238-
else:
239-
print(f"Row {idx}: issue #{num} lacks 'Rejected' label; skipping.")
240-
counts["skipped"] += 1
241-
else:
242-
print(f"Row {idx}: unknown decision '{decision_raw}'; skipping.")
243-
counts["skipped"] += 1
244-
except FileNotFoundError:
245-
print(f"decision.csv not found at: {dec_csv}", file=sys.stderr); sys.exit(1)
246-
247-
print(f"Decisions: accept_commented={counts['accept_commented']}, reject_commented={counts['reject_commented']}, skipped={counts['skipped']} (rows={counts['rows']})")
248-
if dry_run: print("DRY-RUN complete (no changes were made).")
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}")
249188
PY
250189

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

Comments
 (0)