@@ -14,17 +14,149 @@ permissions:
1414
1515jobs :
1616 process-vouch :
17- if : github.repository_owner == 'NVIDIA'
17+ if : >-
18+ github.repository_owner == 'NVIDIA' &&
19+ github.event.comment.body == '/vouch'
1820 runs-on : ubuntu-latest
1921 steps :
20- - uses : actions/checkout@v4
21-
22- - uses : mitchellh/vouch/action/manage-by-discussion@f44860978966ace98fb11aaaa20f2b27d7543e13 # v1
22+ - name : Process /vouch command
23+ uses : actions/github-script@v7
2324 with :
24- discussion-number : ${{ github.event.discussion.number }}
25- comment-node-id : ${{ github.event.comment.node_id }}
26- vouch-keyword : " /vouch"
27- denounce-keyword : " /denounce"
28- unvouch-keyword : " /unvouch"
29- env :
30- GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
25+ script : |
26+ const commenter = context.payload.comment.user.login;
27+ const discussionAuthor = context.payload.discussion.user.login;
28+ const discussionNumber = context.payload.discussion.number;
29+
30+ // --- Helpers ---
31+
32+ async function getDiscussionId() {
33+ const query = `query($owner: String!, $repo: String!, $number: Int!) {
34+ repository(owner: $owner, name: $repo) {
35+ discussion(number: $number) { id }
36+ }
37+ }`;
38+ const { repository } = await github.graphql(query, {
39+ owner: context.repo.owner,
40+ repo: context.repo.repo,
41+ number: discussionNumber,
42+ });
43+ return repository.discussion.id;
44+ }
45+
46+ async function postDiscussionComment(body) {
47+ const discussionId = await getDiscussionId();
48+ const mutation = `mutation($discussionId: ID!, $body: String!) {
49+ addDiscussionComment(input: {discussionId: $discussionId, body: $body}) {
50+ comment { id }
51+ }
52+ }`;
53+ await github.graphql(mutation, { discussionId, body });
54+ }
55+
56+ // --- Authorization ---
57+
58+ let isMaintainer = false;
59+ try {
60+ const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
61+ owner: context.repo.owner,
62+ repo: context.repo.repo,
63+ username: commenter,
64+ });
65+ isMaintainer = ['admin', 'maintain', 'write'].includes(data.permission);
66+ } catch (e) {
67+ console.log(`Permission check failed: ${e.message}`);
68+ }
69+
70+ if (!isMaintainer) {
71+ console.log(`${commenter} does not have maintainer permissions. Ignoring.`);
72+ return;
73+ }
74+
75+ // --- Read VOUCHED.td ---
76+
77+ const filePath = '.github/VOUCHED.td';
78+ const branch = context.payload.repository.default_branch;
79+
80+ let currentContent = '';
81+ let sha = '';
82+ try {
83+ const { data } = await github.rest.repos.getContent({
84+ owner: context.repo.owner,
85+ repo: context.repo.repo,
86+ path: filePath,
87+ ref: branch,
88+ });
89+ currentContent = Buffer.from(data.content, 'base64').toString('utf-8');
90+ sha = data.sha;
91+ } catch (e) {
92+ console.log(`Could not read VOUCHED.td: ${e.message}`);
93+ return;
94+ }
95+
96+ // --- Parse .td format ---
97+
98+ function isVouched(content, username) {
99+ return content
100+ .split('\n')
101+ .map(line => line.trim())
102+ .filter(line => line && !line.startsWith('#') && !line.startsWith('-'))
103+ .some(name => name.toLowerCase() === username.toLowerCase());
104+ }
105+
106+ if (isVouched(currentContent, discussionAuthor)) {
107+ console.log(`${discussionAuthor} is already vouched.`);
108+ await postDiscussionComment(
109+ `@${discussionAuthor} is already vouched. They can submit pull requests.`
110+ );
111+ return;
112+ }
113+
114+ // --- Append username and commit ---
115+
116+ async function commitVouch(content, fileSha) {
117+ const updatedContent = content.trimEnd() + '\n' + discussionAuthor + '\n';
118+ await github.rest.repos.createOrUpdateFileContents({
119+ owner: context.repo.owner,
120+ repo: context.repo.repo,
121+ path: filePath,
122+ message: `chore: vouch ${discussionAuthor}`,
123+ content: Buffer.from(updatedContent).toString('base64'),
124+ sha: fileSha,
125+ branch,
126+ });
127+ }
128+
129+ try {
130+ await commitVouch(currentContent, sha);
131+ } catch (e) {
132+ if (e.status === 409) {
133+ // Concurrent write — re-read and retry once.
134+ console.log('409 conflict. Re-reading VOUCHED.td and retrying.');
135+ const { data: fresh } = await github.rest.repos.getContent({
136+ owner: context.repo.owner,
137+ repo: context.repo.repo,
138+ path: filePath,
139+ ref: branch,
140+ });
141+ const freshContent = Buffer.from(fresh.content, 'base64').toString('utf-8');
142+ if (isVouched(freshContent, discussionAuthor)) {
143+ console.log(`${discussionAuthor} was vouched by a concurrent operation.`);
144+ } else {
145+ await commitVouch(freshContent, fresh.sha);
146+ }
147+ } else {
148+ throw e;
149+ }
150+ }
151+
152+ // --- Confirm ---
153+
154+ await postDiscussionComment([
155+ `@${discussionAuthor} has been vouched by @${commenter}.`,
156+ '',
157+ 'You can now submit pull requests to OpenShell. Welcome aboard.',
158+ '',
159+ 'Please read [CONTRIBUTING.md](https://github.com/NVIDIA/OpenShell/blob/main/CONTRIBUTING.md) before submitting.',
160+ ].join('\n'));
161+
162+ console.log(`Vouched ${discussionAuthor} (approved by ${commenter}).`);
0 commit comments