-
Notifications
You must be signed in to change notification settings - Fork 3k
Expand file tree
/
Copy pathfirst_contrib_cert_generator.yml
More file actions
279 lines (243 loc) · 13.3 KB
/
first_contrib_cert_generator.yml
File metadata and controls
279 lines (243 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
name: Generate Contributor Certificate Preview
# This action triggers automatically when a pull request is closed,
# or can be run manually from the Actions tab.
on:
pull_request_target:
types: [closed]
branches:
- main
workflow_dispatch:
inputs:
contributor_username:
description: 'The GitHub username of the contributor'
required: true
pr_number:
description: 'The pull request number'
required: true
permissions: {}
jobs:
screenshot_and_comment:
# This job runs if the PR was merged or if it's a manual trigger.
# The logic for first-time contributors is handled in a dedicated step below.
if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true }}
runs-on: ubuntu-latest
permissions:
contents: read # Write access for certificate storage
pull-requests: write # Write access to comment on PRs
actions: read # Read access for workflow actions
steps:
# Step 1: Check if this is the contributor's first merged PR.
# This step is the source of truth and will control the execution of subsequent steps.
- name: Check for first merged PR
id: check_first_pr
if: ${{ github.event_name == 'pull_request' }}
uses: actions/github-script@v8
with:
script: |
const author = context.payload.pull_request.user.login;
const query = `repo:${context.repo.owner}/${context.repo.repo} is:pr is:merged author:${author}`;
console.log(`Searching for merged PRs from @${author} with query: "${query}"`);
const result = await github.rest.search.issuesAndPullRequests({ q: query });
const mergedPRs = result.data.total_count;
if (mergedPRs === 1) {
console.log(`SUCCESS: This is the first merged PR from @${author}. Proceeding...`);
core.setOutput('is_first_pr', 'true');
} else {
console.log(`INFO: Skipping certificate generation. @${author} has ${mergedPRs} total merged PRs.`);
core.setOutput('is_first_pr', 'false');
}
# Step 2: Checkout the repository containing the certificate HTML file.
- name: Checkout containers/automation repository
if: ${{ github.event_name == 'workflow_dispatch' || steps.check_first_pr.outputs.is_first_pr == 'true' }}
uses: actions/checkout@v6
with:
repository: containers/automation
path: automation-repo
persist-credentials: false
# Step 3: Update the HTML file locally
- name: Update HTML file
if: ${{ github.event_name == 'workflow_dispatch' || steps.check_first_pr.outputs.is_first_pr == 'true' }}
env:
CONTRIBUTOR_NAME: ${{ github.event.inputs.contributor_username || github.event.pull_request.user.login }}
PR_NUMBER: ${{ github.event.inputs.pr_number || github.event.pull_request.number }}
run: |
HTML_FILE="automation-repo/certificate-generator/certificate_generator.html"
MERGE_DATE=$(date -u +"%B %d, %Y")
sed --sandbox -i -e "/id=\"contributorName\"/s/value=\"[^\"]*\"/value=\"${CONTRIBUTOR_NAME}\"/" ${HTML_FILE} || { echo "ERROR: Failed to update contributor name."; exit 1; }
sed --sandbox -i -e "/id=\"prNumber\"/s/value=\"[^\"]*\"/value=\"#${PR_NUMBER}\"/" ${HTML_FILE} || { echo "ERROR: Failed to update PR number."; exit 1; }
sed --sandbox -i -e "/id=\"mergeDate\"/s/value=\"[^\"]*\"/value=\"${MERGE_DATE}\"/" ${HTML_FILE} || { echo "ERROR: Failed to update merge date."; exit 1; }
# Step 4: Setup Node.js environment
- name: Setup Node.js
if: ${{ github.event_name == 'workflow_dispatch' || steps.check_first_pr.outputs.is_first_pr == 'true' }}
uses: actions/setup-node@v6
with:
node-version: latest
# Step 5: Install Puppeteer
- name: Install Puppeteer
if: ${{ github.event_name == 'workflow_dispatch' || steps.check_first_pr.outputs.is_first_pr == 'true' }}
run: |
npm install puppeteer || { echo "ERROR: Failed to install Puppeteer."; exit 1; }
# Step 6: Take a screenshot of the certificate div
- name: Create and run screenshot script
if: ${{ github.event_name == 'workflow_dispatch' || steps.check_first_pr.outputs.is_first_pr == 'true' }}
run: |
cat <<'EOF' > screenshot.js
const puppeteer = require('puppeteer');
const path = require('path');
(async () => {
const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
const page = await browser.newPage();
const htmlPath = 'file://' + path.resolve('automation-repo/certificate-generator/certificate_generator.html');
await page.goto(htmlPath, { waitUntil: 'networkidle0' });
await page.setViewport({ width: 1080, height: 720 });
const element = await page.$('#certificatePreview');
if (!element) {
console.error('Could not find element #certificatePreview.');
process.exit(1);
}
await element.screenshot({ path: 'certificate.png' });
await browser.close();
console.log('Screenshot saved as certificate.png');
})().catch(err => {
console.error(err);
process.exit(1);
});
EOF
node screenshot.js || { echo "ERROR: Screenshot script failed."; exit 1; }
# Step 7: Upload certificate image to separate repository
- name: Upload certificate to separate repository
if: ${{ github.event_name == 'workflow_dispatch' || steps.check_first_pr.outputs.is_first_pr == 'true' }}
uses: actions/github-script@v8
env:
CONTRIBUTOR_USERNAME: ${{ github.event.inputs.contributor_username }}
USER_LOGIN: ${{ github.event.pull_request.user.login }}
PR_NUMBER: ${{ github.event.inputs.pr_number }}
with:
github-token: ${{ secrets.CERTIFICATES_REPO_TOKEN }}
script: |
const fs = require('fs');
try {
// Check if certificate.png exists
if (!fs.existsSync('certificate.png')) {
throw new Error('certificate.png not found!');
}
// Debug: Check token and repository access
console.log('Testing repository access...');
const certificatesOwner = process.env.CERTIFICATES_REPO_OWNER || context.repo.owner;
const certificatesRepo = process.env.CERTIFICATES_REPO_NAME || 'automation';
// Test repository access first
try {
await github.rest.repos.get({
owner: certificatesOwner,
repo: certificatesRepo
});
console.log(`✅ Repository access confirmed: ${certificatesOwner}/${certificatesRepo}`);
} catch (accessError) {
console.error(`❌ Repository access failed: ${accessError.message}`);
throw new Error(`Cannot access repository ${certificatesOwner}/${certificatesRepo}. Check token permissions and repository existence.`);
}
// Read the certificate image
const imageBuffer = fs.readFileSync('certificate.png');
const base64Content = imageBuffer.toString('base64');
console.log(`Certificate image size: ${imageBuffer.length} bytes`);
// Create a unique filename with timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const contributorName = context.eventName === 'workflow_dispatch'
? process.env.CONTRIBUTOR_USERNAME
: process.env.USER_LOGIN;
const prNumber = context.eventName === 'workflow_dispatch'
? process.env.PR_NUMBER
: context.issue.number;
const filename = `certificates/${contributorName}-${prNumber}-${timestamp}.png`;
// Configuration for the certificates repository
const certificatesBranch = process.env.CERTIFICATES_REPO_BRANCH || 'main';
console.log(`Uploading to repository: ${certificatesOwner}/${certificatesRepo}`);
console.log(`File path: ${filename}`);
console.log(`Branch: ${certificatesBranch}`);
// Upload the file to the certificates repository
await github.rest.repos.createOrUpdateFileContents({
owner: certificatesOwner,
repo: certificatesRepo,
path: filename,
message: `Add certificate for ${contributorName} from ${context.repo.owner}/${context.repo.repo} (PR #${prNumber})\n\nSigned-off-by: Podman Bot <podman.bot@example.com>`,
content: base64Content,
branch: certificatesBranch,
author: {
name: 'Podman Bot',
email: 'podman.bot@example.com'
},
committer: {
name: 'Podman Bot',
email: 'podman.bot@example.com'
}
});
// Create the image URL
const imageUrl = `https://github.com/${certificatesOwner}/${certificatesRepo}/raw/${certificatesBranch}/${filename}`;
console.log(`Certificate uploaded successfully: ${imageUrl}`);
// Store the image URL for the comment step
core.exportVariable('CERTIFICATE_IMAGE_URL', imageUrl);
core.exportVariable('CERTIFICATE_UPLOADED', 'true');
} catch (error) {
console.error('Failed to upload certificate:', error);
console.error('Error details:', error.message);
// Provide helpful error message if it's likely a permissions issue
let errorMsg = error.message;
if (error.status === 404) {
errorMsg += ' (Repository not found - check CERTIFICATES_REPO_OWNER and CERTIFICATES_REPO_NAME environment variables, or ensure the automation repository exists and the token has access)';
} else if (error.status === 403) {
errorMsg += ' (Permission denied - check that CERTIFICATES_REPO_TOKEN has write access to the automation repository)';
}
core.exportVariable('CERTIFICATE_UPLOADED', 'false');
core.exportVariable('UPLOAD_ERROR', errorMsg);
}
# Step 8: Comment on Pull Request with embedded image
- name: Comment with embedded certificate image
if: ${{ github.event_name == 'workflow_dispatch' || steps.check_first_pr.outputs.is_first_pr == 'true' }}
uses: actions/github-script@v8
env:
CONTRIBUTOR_USERNAME: ${{ github.event.inputs.contributor_username }}
USER_LOGIN: ${{ github.event.pull_request.user.login }}
PR_NUMBER: ${{ github.event.inputs.pr_number }}
with:
script: |
try {
let body;
// Check if certificate was uploaded successfully
if (process.env.CERTIFICATE_UPLOADED === 'true') {
const imageUrl = process.env.CERTIFICATE_IMAGE_URL;
console.log(`Using uploaded certificate image: ${imageUrl}`);
// Create the image content with the uploaded image URL
const imageContent = ``;
body = imageContent;
} else {
console.log('Certificate upload failed, providing fallback message');
const errorMsg = process.env.UPLOAD_ERROR || 'Unknown error';
body = `📜 **Certificate Preview**\n\n_Certificate generation completed, but there was an issue uploading the image: ${errorMsg}_\n\nPlease check the workflow logs for more details.`;
}
if (context.eventName === 'workflow_dispatch') {
// Manual trigger case
const contributorName = process.env.CONTRIBUTOR_USERNAME;
const prNumber = process.env.PR_NUMBER;
body = `📜 Certificate preview generated for @${contributorName} (PR #${prNumber}):\n\n${body}`;
} else {
// Auto trigger case for first-time contributors
const username = process.env.USER_LOGIN;
body = `🎉 Congratulations on your first merged pull request, @${username}! Thank you for your contribution.\n\nHere's a preview of your certificate:\n\n${body}`;
}
const issueNumber = context.eventName === 'workflow_dispatch' ?
parseInt(process.env.PR_NUMBER) :
context.issue.number;
await github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
} catch (error) {
core.setFailed(`ERROR: Failed to comment on PR. Details: ${error.message}`);
}
# Step 9: Clean up temporary files
- name: Clean up temporary files
if: ${{ always() && (github.event_name == 'workflow_dispatch' || steps.check_first_pr.outputs.is_first_pr == 'true') }}
run: |
rm -f certificate.png