Skip to content

Commit 9dc031b

Browse files
Update transfer-approved-submission.yml
1 parent 4cd2c6a commit 9dc031b

File tree

1 file changed

+160
-75
lines changed

1 file changed

+160
-75
lines changed
Lines changed: 160 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,189 @@
1-
name: "Transfer Accepted Submission to Private Repo"
1+
name: "Transfer ALL Accepted to Private Repo"
22

33
on:
44
workflow_dispatch:
55
inputs:
6-
issue_number:
7-
description: "Issue number to transfer"
6+
private_owner:
7+
description: "Private repo owner"
88
required: true
9-
type: number
9+
default: pytorch-fdn
10+
type: string
11+
private_name:
12+
description: "Private repo name"
13+
required: true
14+
default: ambassador-program-management
15+
type: string
16+
assignee:
17+
description: "Assignee in private repo"
18+
required: true
19+
default: reginankenchor
20+
type: string
21+
dry_run:
22+
description: "Preview without changing anything"
23+
required: true
24+
default: false
25+
type: boolean
1026

1127
permissions:
1228
contents: read
1329
issues: write
1430

31+
concurrency:
32+
group: transfer-all-accepted
33+
cancel-in-progress: true
34+
1535
jobs:
16-
transfer-submission:
36+
transfer:
1737
runs-on: ubuntu-latest
1838
steps:
19-
- name: Checkout Repo
39+
- name: Checkout
2040
uses: actions/checkout@v4
2141

22-
- name: Transfer Issue to Private Repo
42+
- name: Transfer all Accepted issues
2343
uses: actions/github-script@v7
2444
with:
45+
# IMPORTANT: this token must have write access to BOTH repos
2546
github-token: ${{ secrets.PRIVATE_REPO_TOKEN }}
2647
script: |
27-
const issue_number = parseInt(core.getInput("issue_number"));
48+
const dryRun = core.getInput('dry_run') === 'true';
49+
const privateOwner = core.getInput('private_owner');
50+
const privateName = core.getInput('private_name');
51+
const assignee = core.getInput('assignee');
52+
2853
const repoOwner = context.repo.owner;
2954
const publicRepo = context.repo.repo;
30-
const privateOwner = "pytorch-fdn";
31-
const privateName = "ambassador-program-management";
32-
const assignee = "reginankenchor";
33-
34-
const issue = await github.rest.issues.get({
35-
owner: repoOwner,
36-
repo: publicRepo,
37-
issue_number
38-
});
39-
40-
const hasAcceptedLabel = (issue.data.labels || []).some(
41-
l => String(l.name || "").toLowerCase().includes("accept")
42-
);
43-
44-
if (!hasAcceptedLabel) {
45-
throw new Error(`Issue #${issue_number} does not have an 'Accepted' label. Aborting transfer.`);
55+
56+
const TRANSFER_MARKER = "<!-- transferred-to-private -->";
57+
58+
async function listIssuesWithLabel(label) {
59+
const results = [];
60+
const per_page = 100;
61+
let page = 1;
62+
while (true) {
63+
const { data } = await github.rest.issues.listForRepo({
64+
owner: repoOwner,
65+
repo: publicRepo,
66+
state: "all",
67+
labels: label,
68+
per_page,
69+
page,
70+
});
71+
const issuesOnly = data.filter(i => !i.pull_request);
72+
results.push(...issuesOnly);
73+
if (data.length < per_page) break;
74+
page += 1;
75+
}
76+
return results;
4677
}
4778
48-
const bodyContent = [
49-
"📝 Submission Transferred from Public Repository",
50-
"",
51-
"----------------------------------------",
52-
issue.data.body || "",
53-
"----------------------------------------",
54-
`🔔 @${assignee} — this submission has been accepted and is now ready for program-level follow-up.`
55-
].join("\n\n");
56-
57-
const newIssue = await github.rest.issues.create({
58-
owner: privateOwner,
59-
repo: privateName,
60-
title: issue.data.title,
61-
body: bodyContent,
62-
assignees: [assignee]
63-
});
64-
65-
// Invite the issue author to the private repo with read access
66-
const applicant = issue.data.user?.login;
67-
if (applicant) {
68-
try {
69-
await github.rest.repos.addCollaborator({
70-
owner: privateOwner,
71-
repo: privateName,
72-
username: applicant,
73-
permission: "pull" // read-only access
79+
async function hasMarkerComment(owner, repo, issue_number) {
80+
const per_page = 100;
81+
let page = 1;
82+
while (true) {
83+
const { data } = await github.rest.issues.listComments({
84+
owner, repo, issue_number, per_page, page
7485
});
75-
} catch (e) {
76-
// ignore invite errors (already invited, org policy, etc.)
86+
if (data.some(c => (c.body || "").includes(TRANSFER_MARKER))) return true;
87+
if (data.length < per_page) break;
88+
page += 1;
7789
}
90+
return false;
7891
}
7992
80-
const confirmation = [
81-
"✅ This submission has been **accepted** and transferred to the private program management repository.",
82-
"",
83-
`🔗 [View it here](${newIssue.data.html_url})`,
84-
"",
85-
"ℹ️ This issue remains open here for visibility, but ongoing tracking happens in the private repository."
86-
].join("\n\n");
87-
88-
// --- Ensure the public issue is unlocked before commenting ---
89-
try {
90-
await github.rest.issues.unlock({
91-
owner: repoOwner,
92-
repo: publicRepo,
93-
issue_number
94-
});
95-
} catch (e) {
96-
// ignore if already unlocked
93+
function buildPrivateBody(publicIssue) {
94+
return [
95+
"📝 Submission Transferred from Public Repository",
96+
"",
97+
"----------------------------------------",
98+
publicIssue.body || "",
99+
"----------------------------------------",
100+
`Source: ${publicIssue.html_url}`,
101+
`Original Author: @${publicIssue.user?.login || "unknown"}`,
102+
"",
103+
`🔔 @${assignee} — this submission has been accepted and is now ready for program-level follow-up.`
104+
].join("\n\n");
105+
}
106+
107+
const accepted = await listIssuesWithLabel("Accepted");
108+
core.info(`Found ${accepted.length} issues labeled 'Accepted'.`);
109+
110+
let transferred = 0, skipped = 0, failed = 0;
111+
112+
for (const pub of accepted) {
113+
const number = pub.number;
114+
115+
// Skip if already transferred (marker present)
116+
const already = await hasMarkerComment(repoOwner, publicRepo, number);
117+
if (already) {
118+
core.info(`#${number}: already transferred (marker found). Skipping.`);
119+
skipped += 1;
120+
continue;
121+
}
122+
123+
// Create issue in private repo
124+
const body = buildPrivateBody(pub);
125+
126+
if (dryRun) {
127+
core.info(`[DRY-RUN] Would create private issue for #${number} and invite @${pub.user?.login}`);
128+
} else {
129+
let newIssue;
130+
try {
131+
newIssue = await github.rest.issues.create({
132+
owner: privateOwner,
133+
repo: privateName,
134+
title: pub.title,
135+
body,
136+
assignees: [assignee]
137+
});
138+
} catch (e) {
139+
core.warning(`#${number}: creating private issue failed: ${e.message}`);
140+
failed += 1;
141+
continue;
142+
}
143+
144+
// Invite the applicant (read access)
145+
const applicant = pub.user?.login;
146+
if (applicant) {
147+
try {
148+
await github.rest.repos.addCollaborator({
149+
owner: privateOwner,
150+
repo: privateName,
151+
username: applicant,
152+
permission: "pull"
153+
});
154+
} catch (e) {
155+
// ignore invite errors (already invited/org policy/etc.)
156+
core.info(`#${number}: invite @${applicant} ignored: ${e.message}`);
157+
}
158+
}
159+
160+
// Ensure public issue is unlocked before commenting; leave it unlocked
161+
try {
162+
await github.rest.issues.unlock({
163+
owner: repoOwner, repo: publicRepo, issue_number: number
164+
});
165+
} catch (e) {
166+
// ignore if already unlocked or not lockable
167+
}
168+
169+
const confirmation = [
170+
"✅ This submission has been **accepted** and transferred to the private program management repository.",
171+
"",
172+
`🔗 **Private tracking issue:** ${newIssue.data.html_url}`,
173+
"",
174+
TRANSFER_MARKER
175+
].join("\n");
176+
177+
try {
178+
await github.rest.issues.createComment({
179+
owner: repoOwner, repo: publicRepo, issue_number: number, body: confirmation
180+
});
181+
} catch (e) {
182+
core.warning(`#${number}: public confirmation comment failed: ${e.message}`);
183+
}
184+
185+
transferred += 1;
186+
}
97187
}
98188
99-
await github.rest.issues.createComment({
100-
owner: repoOwner,
101-
repo: publicRepo,
102-
issue_number,
103-
body: confirmation
104-
});
189+
core.info(`Done. transferred=${transferred}, skipped=${skipped}, failed=${failed}, dry_run=${dryRun}`);

0 commit comments

Comments
 (0)