Skip to content

App meal creation support #355

App meal creation support

App meal creation support #355

Workflow file for this run

name: PR Validation
on:
pull_request:
types: [opened, edited, synchronize, reopened]
permissions:
contents: read
jobs:
validate-pr:
name: Validate PR Requirements
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check PR Description
id: check_description
uses: actions/github-script@v7
with:
script: |
const prBody = context.payload.pull_request.body || '';
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
const changedFiles = files.map(f => f.filename);
// Change detection
const hasFrontendChanges = changedFiles.some(f =>
f.startsWith('SparkyFitnessFrontend/') || f.startsWith('src/')
);
const hasBackendChanges = changedFiles.some(f =>
f.startsWith('SparkyFitnessServer/')
);
const hasMobileChanges = changedFiles.some(f =>
f.startsWith('SparkyFitnessMobile/')
);
// Translation change detection
const localesRoot = 'SparkyFitnessFrontend/public/locales/';
const nonEnLocaleFiles = changedFiles.filter(f =>
f.startsWith(localesRoot) && !f.startsWith(`${localesRoot}en/`)
);
const hasEnTranslationChange = changedFiles.some(f =>
f === `${localesRoot}en/translation.json`
);
// SQL change detection
const hasSQLChanges = changedFiles.some(f =>
f.startsWith('SparkyFitnessServer/') && f.endsWith('.sql')
);
const isNewFeature =
prBody.includes('[x] New Feature') ||
prBody.includes('[X] New Feature');
const checked = str =>
prBody.includes(`[x] **${str}**`) ||
prBody.includes(`[X] **${str}**`);
let errors = [];
let warnings = [];
// --- Integrity (all PRs) ---
if (!checked('[MANDATORY - ALL] Integrity & License')) {
errors.push('**[MANDATORY]** Check the "Integrity & License" checkbox.');
}
// --- Alignment (new features) ---
if (isNewFeature && !checked('[MANDATORY for new feature] Alignment')) {
errors.push('**[MANDATORY for new features]** Check the "Alignment" checkbox.');
}
// --- Frontend quality ---
if (hasFrontendChanges && !checked('[MANDATORY for Frontend changes] Quality')) {
errors.push('**[MANDATORY for Frontend changes]** Check the "Quality" checkbox (run `pnpm run validate`).');
}
// --- Translations ---
// Hard error: non-en locale files must never be modified by contributors
if (nonEnLocaleFiles.length > 0) {
errors.push(
`**[MANDATORY]** Only the \`en\` translation file may be modified. ` +
`Remove changes to: ${nonEnLocaleFiles.map(f => '`' + f + '`').join(', ')}. ` +
`Non-English locales are maintained by project maintainers.`
);
}
// Require checkbox when en/translation.json is touched
if (hasEnTranslationChange && !checked('[MANDATORY for Frontend changes] Translations')) {
errors.push(
'**[MANDATORY for Frontend changes]** You modified `en/translation.json`. ' +
'Check the "Translations" checkbox.'
);
}
// --- Backend quality ---
if (hasBackendChanges && !checked('[MANDATORY for Backend changes] Code Quality')) {
errors.push('**[MANDATORY for Backend changes]** Check the "Code Quality" checkbox.');
}
// --- Database security ---
if (hasSQLChanges && !checked('[MANDATORY for Backend changes] Database Security')) {
errors.push(
'**[MANDATORY for Backend changes]** SQL file changes detected. ' +
'Check the "Database Security" checkbox and confirm `rls_policies.sql` is updated for any new user-specific tables.'
);
}
// --- Screenshots (self-reported) ---
// UI change detection is unreliable (files in pages/ aren't always visual changes),
// so we only validate that screenshots are actually present if the contributor checks the box.
const screenshotsChecked = checked('[MANDATORY for UI changes] Screenshots');
if (screenshotsChecked) {
// Only look for image markdown, HTML tags, or generic URLs
const hasImages = prBody.includes('![') || prBody.includes('<img') || prBody.includes('http');
if (!hasImages) {
errors.push(
'**[MANDATORY for UI changes]** You checked the Screenshots box, but no images were found in the PR body. Please attach them.'
);
}
}
// --- Description quality ---
const descriptionBody = prBody.split('## Description')[1] || '';
if (!prBody.includes('## Description') || descriptionBody.trim().length < 20) {
warnings.push('Please provide a meaningful description of your changes.');
}
// --- Linked issue ---
if (!prBody.match(/Linked Issue:\s*#\d+/)) {
warnings.push('Please link a related GitHub issue (`Linked Issue: #123`).');
}
// --- Build summary ---
let message = '## PR Validation Results\n\n';
message += '### Change Detection\n';
if (hasFrontendChanges) message += '- Frontend changes detected\n';
if (hasBackendChanges) message += '- Backend changes detected\n';
if (hasMobileChanges) message += '- Mobile changes detected\n';
if (hasEnTranslationChange) message += '- `en` translation file modified\n';
if (nonEnLocaleFiles.length) message += `- ⛔ Non-English locale files modified: ${nonEnLocaleFiles.join(', ')}\n`;
if (hasSQLChanges) message += '- SQL changes detected\n';
message += '\n';
if (errors.length > 0) {
message += '### ❌ Required Actions\n\n';
message += errors.map(e => `- ${e}`).join('\n') + '\n\n';
errors.forEach(e => core.error(e));
}
if (warnings.length > 0) {
message += '### ⚠️ Recommendations\n\n';
message += warnings.map(w => `- ${w}`).join('\n') + '\n\n';
warnings.forEach(w => core.warning(w));
}
if (errors.length === 0 && warnings.length === 0) {
message += '### ✅ All checks passed. Thank you!\n';
}
await core.summary.addRaw(message).write();
if (errors.length > 0) {
core.setFailed('Required PR checks are missing. See the summary for details.');
}
- name: Validate PR Type Selected
uses: actions/github-script@v7
with:
script: |
const prBody = context.payload.pull_request.body || '';
const hasIssue = prBody.includes('[x] Issue') || prBody.includes('[X] Issue');
const hasFeature = prBody.includes('[x] New Feature') || prBody.includes('[X] New Feature');
const hasRefactor = prBody.includes('[x] Refactor') || prBody.includes('[X] Refactor');
const hasDoc = prBody.includes('[x] Documentation') || prBody.includes('[X] Documentation');
if (!hasIssue && !hasFeature && !hasRefactor && !hasDoc) {
const message = 'Please select a PR type (Issue, New Feature, Refactor, or Documentation).';
core.warning(message);
await core.summary.addRaw(`\n\n### ⚠️ PR Type Missing\n${message}`).write();
}