Skip to content

add image functionality #2

add image functionality

add image functionality #2

name: Create Project Page PR from Issue
on:
issues:
types: [opened]
jobs:
create-project-page-pr:
# Only run for issues carrying the project-submission label
if: contains(github.event.issue.labels.*.name, 'project-submission')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# ── Install YAML parsing tool and utilities ─────────────────────────────
- name: Install dependencies
run: |
npm install -g js-yaml
# ── Parse issue body ─────────────────────────────────────────────────────
- name: Parse issue fields
id: parse
uses: actions/github-script@v7
with:
script: |
const body = context.payload.issue.body ?? '';
// Helper: extract the text under a given issue-form heading
function extract(label) {
// Matches "### Label\n\nvalue" blocks produced by GitHub issue forms
const re = new RegExp(
`### ${label}\\s*\\n+([\\s\\S]*?)(?=\\n### |$)`,
'i'
);
const m = body.match(re);
if (!m) return '';
const val = m[1].trim();
// GitHub renders blank optional fields as "_No response_"
return val === '_No response_' ? '' : val;
}
const title = extract('Project Title');
const summary = extract('One-Line Summary');
const origin = extract('Origin \\(Optional\\)');
const tagsRaw = extract('Project Tags');
const description = extract('Project Description');
const linksRaw = extract('Relevant Links \\(Optional\\)');
const status = extract('Project Status');
const year = extract('Year \\(if Past Project\\)');
const mainWorkArea = extract('Main Work Area Category');
// Parse tags from checkboxes (format: "- [x] label" or "- [ ] label")
const tags = [];
const tagMatches = tagsRaw.match(/^\s*-\s*\[x\]\s*(.+)$/gm) || [];
tagMatches.forEach(match => {
const tag = match.replace(/^\s*-\s*\[x\]\s*/, '').trim();
if (tag) tags.push(tag);
});
// Parse links: "Name: URL" format
const links = [];
if (linksRaw) {
const linkLines = linksRaw.split('\n').filter(l => l.trim());
linkLines.forEach(line => {
if (line.includes(':')) {
const idx = line.indexOf(':');
const name = line.slice(0, idx).trim();
const url = line.slice(idx + 1).trim();
if (name && url) links.push({ name, url });
}
});
}
// Generate slug from title (lowercase, replace spaces/special chars with hyphens)
const slug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
// Expose as outputs
core.setOutput('title', title);
core.setOutput('summary', summary);
core.setOutput('origin', origin);
core.setOutput('tags', JSON.stringify(tags));
core.setOutput('description', description);
core.setOutput('links', JSON.stringify(links));
core.setOutput('status', status);
core.setOutput('year', year);
core.setOutput('main_work_area', mainWorkArea);
core.setOutput('slug', slug);
# ── Validate required fields ─────────────────────────────────────────────
- name: Validate inputs
id: validate
uses: actions/github-script@v7
with:
script: |
const title = '${{ steps.parse.outputs.title }}'.trim();
const summary = '${{ steps.parse.outputs.summary }}'.trim();
const description = '${{ steps.parse.outputs.description }}'.trim();
const status = '${{ steps.parse.outputs.status }}'.trim();
const year = '${{ steps.parse.outputs.year }}'.trim();
const mainWorkArea = '${{ steps.parse.outputs.main_work_area }}'.trim();
const tags = JSON.parse('${{ steps.parse.outputs.tags }}');
const errors = [];
if (!title) errors.push('❌ Project Title is required');
if (!summary) errors.push('❌ One-Line Summary is required');
if (!description) errors.push('❌ Project Description is required');
if (!status) errors.push('❌ Project Status is required');
if (!mainWorkArea) errors.push('❌ Main Work Area Category is required');
if (tags.length === 0) errors.push('❌ At least one tag is required');
// Validate status and year
if (status && !['Current Project', 'Past Project'].includes(status)) {
errors.push(`❌ Invalid Project Status: "${status}". Must be "Current Project" or "Past Project"`);
}
if (status === 'Past Project' && !year) {
errors.push('❌ Year is required when Project Status is "Past Project"');
}
if (year && isNaN(parseInt(year))) {
errors.push(`❌ Year must be a valid number, got: "${year}"`);
}
// Valid work areas
const validAreas = [
'Predictive Analytics Products',
'Data Science for Linked/Longitudinal Data',
'Natural Language Processing Products',
'Data Science Capability',
'Research & Development'
];
if (mainWorkArea && !validAreas.includes(mainWorkArea)) {
errors.push(`❌ Invalid Main Work Area. Valid options: ${validAreas.join(', ')}`);
}
if (errors.length > 0) {
core.error('Validation failed:\n' + errors.join('\n'));
core.setOutput('valid', 'false');
core.setOutput('error_message', errors.join('\n'));
} else {
core.setOutput('valid', 'true');
}
# ── Stop if validation failed ────────────────────────────────────────────
- name: Comment on issue if validation fails
if: steps.validate.outputs.valid == 'false'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## ❌ Validation Failed\n\nYour project submission has the following issues:\n\n${{ steps.validate.outputs.error_message }}\n\nPlease edit the issue and correct these fields.`
});
- name: Exit if validation failed
if: steps.validate.outputs.valid == 'false'
run: exit 1
# ── Create branch and checkout ──────────────────────────────────────────
- name: Create and checkout feature branch
run: |
git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
BRANCH_NAME="project-submission/${{ steps.parse.outputs.slug }}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
# ── Generate project markdown file ──────────────────────────────────────
- name: Generate markdown file
id: generate_md
uses: actions/github-script@v7
env:
TITLE: ${{ steps.parse.outputs.title }}
SUMMARY: ${{ steps.parse.outputs.summary }}
ORIGIN: ${{ steps.parse.outputs.origin }}
TAGS: ${{ steps.parse.outputs.tags }}
DESCRIPTION: ${{ steps.parse.outputs.description }}
LINKS: ${{ steps.parse.outputs.links }}
SLUG: ${{ steps.parse.outputs.slug }}
with:
script: |
const fs = require('fs');
const path = require('path');
const title = process.env.TITLE;
const summary = process.env.SUMMARY;
const origin = process.env.ORIGIN || 'Data Science Team';
const tagsRaw = JSON.parse(process.env.TAGS);
const description = process.env.DESCRIPTION;
const linksRaw = JSON.parse(process.env.LINKS);
const slug = process.env.SLUG;
// Clean tags: remove prefixes like "Domain: ", "Technique: ", etc.
const tags = tagsRaw.map(t => {
// Remove prefix like "Domain: ", "Technique: ", etc.
return t.replace(/^[^:]+:\s*/, '').trim();
});
// Generate frontmatter
let frontmatter = '---\n';
frontmatter += `title: '${title.replace(/'/g, "\\'")}'\n`;
frontmatter += `summary: '${summary.replace(/'/g, "\\'")}'\n`;
frontmatter += `origin: '${origin.replace(/'/g, "\\'")}'\n`;
frontmatter += `tags: [${tags.map(t => `'${t.replace(/'/g, "\\'")}'`).join(', ')}]\n`;
frontmatter += '---\n\n';
// Build document
let content = frontmatter;
// Add description (assumes it contains proper markdown structure)
content += description + '\n\n';
// Add links table if present
if (linksRaw.length > 0) {
content += '## Outputs & Links\n\n';
content += 'Output | Link\n';
content += '---|---\n';
linksRaw.forEach(link => {
content += `${link.name} | [Link](${link.url})\n`;
});
content += '\n';
}
// Write to file
const filePath = path.join('docs/our_work', `${slug}.md`);
fs.writeFileSync(filePath, content);
console.log(`Generated: ${filePath}`);
core.setOutput('md_path', filePath);
# ── Check if slug already exists ────────────────────────────────────────
- name: Check for duplicate slug
id: check_duplicate
run: |
if [ -f "docs/our_work/${{ steps.parse.outputs.slug }}.md" ]; then
# File already existed before we created it
if git ls-files --error-unmatch "docs/our_work/${{ steps.parse.outputs.slug }}.md" > /dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
fi
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Comment if duplicate slug
if: steps.check_duplicate.outputs.exists == 'true'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## ⚠️ Project Already Exists\n\nA project with the slug \`${{ steps.parse.outputs.slug }}\` already exists. Please use a different project title or update the existing project page instead.`
});
- name: Exit if duplicate
if: steps.check_duplicate.outputs.exists == 'true'
run: exit 1
# ── Update mkdocs.yml ───────────────────────────────────────────────────
- name: Update mkdocs.yml
id: update_mkdocs
uses: actions/github-script@v7
env:
TITLE: ${{ steps.parse.outputs.title }}
SLUG: ${{ steps.parse.outputs.slug }}
STATUS: ${{ steps.parse.outputs.status }}
YEAR: ${{ steps.parse.outputs.year }}
WORK_AREA: ${{ steps.parse.outputs.main_work_area }}
with:
script: |
const fs = require('fs');
const yaml = require('js-yaml');
const mkdocsPath = 'mkdocs.yml';
const content = fs.readFileSync(mkdocsPath, 'utf8');
const config = yaml.load(content);
const title = process.env.TITLE;
const slug = process.env.SLUG;
const status = process.env.STATUS;
const year = process.env.YEAR;
const workArea = process.env.WORK_AREA;
const newEntry = `${title}: our_work/${slug}.md`;
// Navigate to Projects nav section
const projectsSection = config.nav.find(item => item.Projects);
if (!projectsSection) {
core.setFailed('Could not find Projects section in mkdocs.yml');
return;
}
const projectsNav = projectsSection.Projects;
// Add to Past/Current Projects
const pastCurrentSection = projectsNav.find(item => item['Past/Current Projects']);
if (pastCurrentSection && pastCurrentSection['Past/Current Projects']) {
if (status === 'Current Project') {
// Add to Current Projects
const currentProjects = pastCurrentSection['Past/Current Projects'].find(
item => item['Current Projects']
);
if (currentProjects && Array.isArray(currentProjects['Current Projects'])) {
currentProjects['Current Projects'].push(newEntry);
console.log(`Added to Current Projects`);
}
} else if (status === 'Past Project') {
// Add to Past Projects under the year
const pastProjects = pastCurrentSection['Past/Current Projects'].find(
item => item['Past Projects']
);
if (pastProjects && Array.isArray(pastProjects['Past Projects'])) {
// Find or create year entry
let yearEntry = pastProjects['Past Projects'].find(item => item[year]);
if (!yearEntry) {
yearEntry = { [year]: [] };
pastProjects['Past Projects'].push(yearEntry);
}
yearEntry[year].push(newEntry);
console.log(`Added to Past Projects > ${year}`);
}
}
}
// Add to Main Work Areas
const mainWorkAreasSection = projectsNav.find(item => item['Main Work Areas']);
if (mainWorkAreasSection && mainWorkAreasSection['Main Work Areas']) {
const workAreaEntry = mainWorkAreasSection['Main Work Areas'].find(
item => item[workArea]
);
if (workAreaEntry && Array.isArray(workAreaEntry[workArea])) {
workAreaEntry[workArea].push(newEntry);
console.log(`Added to Main Work Areas > ${workArea}`);
} else {
core.warning(`Could not find work area: ${workArea}`);
}
}
// Write back with careful formatting
const updatedContent = yaml.dump(config, {
lineWidth: -1,
indent: 2,
noRefs: true
});
fs.writeFileSync(mkdocsPath, updatedContent);
console.log('Updated mkdocs.yml');
# ── Handle images (for now, create placeholder) ──────────────────────────
- name: Instructions for images comment
run: |
echo "Images will be processed from issue attachments."
echo "Github issue form file uploads appear as links in the issue body."
echo "These will need to be extracted and downloaded."
# ── Commit changes ──────────────────────────────────────────────────────
- name: Commit changes
run: |
git add docs/our_work/${{ steps.parse.outputs.slug }}.md
git add mkdocs.yml
git commit -m "Add project page: ${{ steps.parse.outputs.title }}"
# ── Push branch and create PR ───────────────────────────────────────────
- name: Push branch
run: |
git push origin ${{ env.BRANCH_NAME }}
- name: Create Pull Request
id: cpr
uses: actions/github-script@v7
with:
script: |
const pr = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
head: '${{ env.BRANCH_NAME }}',
base: 'main',
title: 'Add project page: ${{ steps.parse.outputs.title }}',
body: `## Project Submission: ${{ steps.parse.outputs.title }}
### Summary
${{ steps.parse.outputs.summary }}

Check failure on line 396 in .github/workflows/project-submission-pr.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/project-submission-pr.yml

Invalid workflow file

You have an error in your yaml syntax on line 396
### Details
- **Status**: ${{ steps.parse.outputs.status }}
- **Year**: ${{ steps.parse.outputs.year || 'N/A' }}
- **Main Work Area**: ${{ steps.parse.outputs.main_work_area }}
- **Origin**: ${{ steps.parse.outputs.origin }}
### Changes
- Added: \`docs/our_work/${{ steps.parse.outputs.slug }}.md\`
- Updated: \`mkdocs.yml\` (added to Current/Past Projects and Work Area sections)
### Next Steps
1. Review the changes in this PR
2. If images need to be uploaded, please add them to \`docs/images/our_work/${{ steps.parse.outputs.slug }}/\`
3. Once approved, merge this PR
4. The website will be updated automatically
Closes #${{ github.event.issue.number }}`
});
core.setOutput('pr_number', pr.data.number);
core.setOutput('pr_url', pr.data.html_url);
# ── Comment on original issue ───────────────────────────────────────────
- name: Comment on issue with PR link
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## ✅ Project Submission Processed
Great! Your project page has been generated and a PR has been created.
**Pull Request**: #${{ steps.cpr.outputs.pr_number }}
[View PR](${{ steps.cpr.outputs.pr_url }})
### Next Steps
1. Review the generated project page markdown
2. If you need to upload images, download them to \`docs/images/our_work/${{ steps.parse.outputs.slug }}/\` and commit them to the PR
3. The team will review and merge when ready
**Note**: Images should be uploaded to the PR branch at \`docs/images/our_work/${{ steps.parse.outputs.slug }}/\` if multiple images, or \`docs/images/our_work/${{ steps.parse.outputs.slug }}_[filename]\` if a single image.`
});