Skip to content

Commit 0bcb8e9

Browse files
Update ambassador-comments.yml
1 parent 9e19ce2 commit 0bcb8e9

File tree

1 file changed

+0
-249
lines changed

1 file changed

+0
-249
lines changed
Lines changed: 0 additions & 249 deletions
Original file line numberDiff line numberDiff line change
@@ -1,250 +1 @@
1-
name: "Ambassador comments (decisions only)"
2-
3-
on:
4-
workflow_dispatch:
5-
inputs:
6-
private_repo:
7-
description: "Private repo containing the CSVs (owner/name)"
8-
required: true
9-
default: pytorch-fdn/ambassador-program-management
10-
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
26-
dry_run:
27-
description: "Preview without posting"
28-
required: true
29-
default: false
30-
type: boolean
31-
32-
permissions:
33-
contents: read
34-
issues: write
35-
36-
concurrency:
37-
group: ambassador-comments
38-
cancel-in-progress: true
39-
40-
jobs:
41-
comment:
42-
runs-on: ubuntu-latest
43-
steps:
44-
- name: Checkout PUBLIC repo
45-
uses: actions/checkout@v4
46-
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-
55-
- name: Set up Python
56-
uses: actions/setup-python@v5
57-
with:
58-
python-version: "3.11"
59-
60-
- name: Install dependencies
61-
run: pip install requests
62-
63-
- name: Post comments from decision.csv
64-
env:
65-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
66-
REPO_FULL: ${{ github.repository }}
67-
DEC_CSV_ABS: ${{ github.workspace }}/private-src/${{ inputs.decision_csv_path }}
68-
UNLOCK_LOCKED: ${{ inputs.unlock_locked_issues }}
69-
DRY_RUN: ${{ inputs.dry_run }}
70-
run: |
71-
python - <<'PY'
72-
import os, csv, re, sys, urllib.parse
73-
import requests
74-
from requests.adapters import HTTPAdapter, Retry
75-
76-
token = os.getenv("GITHUB_TOKEN")
77-
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")
81-
82-
if not token or not repo_full:
83-
print("Missing GITHUB_TOKEN or REPO_FULL", file=sys.stderr); sys.exit(1)
84-
85-
owner, repo = repo_full.split("/", 1)
86-
87-
# session with retries
88-
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"
136-
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
144-
url = r.links.get("next",{}).get("url")
145-
146-
# 2) try to comment
147-
if dry_run:
148-
print(f"DRY-RUN: would comment on #{num}:\n{body}\n---")
149-
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).")
249-
PY
2501

0 commit comments

Comments
 (0)