Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 269 additions & 0 deletions .github/workflows/first_contrib_cert_generator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
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:
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 needed for this workflow.
permissions:
contents: read # Write access for certificate storage
pull-requests: write # Write access to comment on PRs
actions: read # Read access for workflow actions

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
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@v7
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@v4
with:
repository: containers/automation
path: automation-repo

# 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' }}
run: |
HTML_FILE="automation-repo/certificate-generator/certificate_generator.html"
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 }}"
MERGE_DATE=$(date -u +"%B %d, %Y")

sed -i "/id=\"contributorName\"/s/value=\"[^\"]*\"/value=\"${CONTRIBUTOR_NAME}\"/" ${HTML_FILE} || { echo "ERROR: Failed to update contributor name."; exit 1; }
sed -i "/id=\"prNumber\"/s/value=\"[^\"]*\"/value=\"#${PR_NUMBER}\"/" ${HTML_FILE} || { echo "ERROR: Failed to update PR number."; exit 1; }
sed -i "/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@v4
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@v7
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'
? '${{ github.event.inputs.contributor_username }}'
: '${{ github.event.pull_request.user.login }}';
const prNumber = context.eventName === 'workflow_dispatch'
? '${{ github.event.inputs.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 <[email protected]>`,
content: base64Content,
branch: certificatesBranch,
author: {
name: 'Podman Bot',
email: '[email protected]'
},
committer: {
name: 'Podman Bot',
email: '[email protected]'
}
});

// 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@v7
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 = `![Certificate Preview](${imageUrl})`;
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 = '${{ github.event.inputs.contributor_username }}';
const prNumber = '${{ github.event.inputs.pr_number }}';
body = `📜 Certificate preview generated for @${contributorName} (PR #${prNumber}):\n\n${body}`;
} else {
// Auto trigger case for first-time contributors
const username = '${{ github.event.pull_request.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('${{ github.event.inputs.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