Add PostPeer #96
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Validate Resources & Request Copilot Review | |
| on: | |
| pull_request_target: | |
| paths: | |
| - resources/** | |
| - db/** | |
| - README.md | |
| types: [opened, synchronize] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| # Job to check for changes to auto-generated files (db/ and README.md) | |
| check-auto-generated: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has_db_changes: ${{ steps.check.outputs.has_db_changes }} | |
| has_readme_changes: ${{ steps.check.outputs.has_readme_changes }} | |
| steps: | |
| - name: Check for auto-generated file changes | |
| id: check | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const pr = context.payload.pull_request; | |
| // Get list of changed files | |
| const { data: files } = await github.rest.pulls.listFiles({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: pr.number | |
| }); | |
| // Check for db folder changes | |
| const dbChanges = files.some(f => f.filename.startsWith('db/')); | |
| core.setOutput('has_db_changes', dbChanges.toString()); | |
| // Check for README changes | |
| const readmeChanges = files.some(f => f.filename.toLowerCase() === 'readme.md'); | |
| core.setOutput('has_readme_changes', readmeChanges.toString()); | |
| - name: Post comment about db folder changes | |
| if: steps.check.outputs.has_db_changes == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const commentBody = 'Hi, and thank you for the contribution.\n\n' + | |
| 'The `/db` folder is auto-generated, and changes must be made in the `/resources` folder.\n' + | |
| 'Could you please amend this PR?\n' + | |
| 'Our [CONTRIBUTING](https://github.com/marcelscruz/dev-resources/blob/main/CONTRIBUTING.md) guide has instructions that might help you.\n\n' + | |
| 'Thanks!'; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: commentBody | |
| }); | |
| - name: Post comment about README changes | |
| if: steps.check.outputs.has_readme_changes == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const commentBody = 'Hi, and thank you for the contribution.\n\n' + | |
| 'The README file is auto-generated, and changes must be made in the `/resources` folder.\n' + | |
| 'Could you please amend this PR?\n' + | |
| 'Our [CONTRIBUTING](https://github.com/marcelscruz/dev-resources/blob/main/CONTRIBUTING.md) guide has instructions that might help you.\n\n' + | |
| 'Thanks!'; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: commentBody | |
| }); | |
| validate-and-request-copilot-review: | |
| runs-on: ubuntu-latest | |
| needs: check-auto-generated | |
| # Only run if no auto-generated files were changed | |
| if: needs.check-auto-generated.outputs.has_db_changes != 'true' && needs.check-auto-generated.outputs.has_readme_changes != 'true' | |
| steps: | |
| - name: Checkout PR head | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ github.event.pull_request.head.repo.full_name }} | |
| ref: ${{ github.event.pull_request.head.ref }} | |
| fetch-depth: 0 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - name: Run resource validation | |
| id: validate | |
| run: | | |
| node scripts/validate-resources.js json 2>&1 | tee validation-output.json | |
| EXIT_CODE=${PIPESTATUS[0]} | |
| echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT | |
| continue-on-error: true | |
| - name: Run TypeScript check | |
| id: typescript | |
| run: | | |
| npx tsc --noEmit 2>&1 | tee typescript-output.txt || true | |
| if grep -q "error TS" typescript-output.txt; then | |
| echo "has_errors=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_errors=false" >> $GITHUB_OUTPUT | |
| fi | |
| continue-on-error: true | |
| - name: Report validation results and request Copilot review | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| const exitCode = '${{ steps.validate.outputs.exit_code }}'; | |
| const tsHasErrors = '${{ steps.typescript.outputs.has_errors }}' === 'true'; | |
| let validationData; | |
| try { | |
| const rawOutput = fs.readFileSync('validation-output.json', 'utf8'); | |
| validationData = JSON.parse(rawOutput); | |
| } catch (e) { | |
| validationData = null; | |
| } | |
| let commentBody = '## π€ Automated PR Validation\n\n'; | |
| // Validation passed | |
| if (exitCode === '0' && !tsHasErrors) { | |
| commentBody += '### β All Checks Passed!\n\n'; | |
| commentBody += 'Your resource submission looks good. '; | |
| commentBody += 'GitHub Copilot will review the code quality and style.\n\n'; | |
| } else { | |
| // Validation failed - report issues for Copilot to review | |
| commentBody += '### β οΈ Issues Found\n\n'; | |
| commentBody += 'The following issues were detected. '; | |
| commentBody += '**GitHub Copilot** will review this PR and report any additional issues.\n\n'; | |
| if (validationData && validationData.errors && validationData.errors.length > 0) { | |
| commentBody += '#### Resource Validation Errors\n\n'; | |
| // Group errors by type for better readability | |
| const errorsByType = {}; | |
| for (const error of validationData.errors) { | |
| if (!errorsByType[error.type]) { | |
| errorsByType[error.type] = []; | |
| } | |
| errorsByType[error.type].push(error); | |
| } | |
| for (const [type, errors] of Object.entries(errorsByType)) { | |
| const typeLabels = { | |
| 'invalid_category': 'π·οΈ Invalid Categories', | |
| 'alphabetical_order': 'π€ Alphabetical Order', | |
| 'missing_description': 'π Missing Description', | |
| 'missing_url': 'π Missing URL', | |
| 'invalid_url_protocol': 'π Invalid URL Protocol', | |
| 'duplicate_url': 'π Duplicate URL', | |
| 'too_many_categories': 'π Too Many Categories', | |
| 'wrong_file': 'π Wrong File Location', | |
| 'typescript_syntax': 'β οΈ Syntax Error' | |
| }; | |
| commentBody += `**${typeLabels[type] || type}**\n\n`; | |
| for (const error of errors) { | |
| commentBody += `- \`${error.location}\`: ${error.message}\n`; | |
| } | |
| commentBody += '\n'; | |
| } | |
| } | |
| if (tsHasErrors) { | |
| const tsOutput = fs.readFileSync('typescript-output.txt', 'utf8'); | |
| commentBody += '#### TypeScript Errors\n\n'; | |
| commentBody += '```\n' + tsOutput.substring(0, 2000) + '\n```\n\n'; | |
| } | |
| // Instructions for Copilot | |
| commentBody += '---\n\n'; | |
| commentBody += '### π Request for GitHub Copilot\n\n'; | |
| commentBody += 'Review this PR and report any issues you find, including:\n\n'; | |
| commentBody += '- Code quality and style issues\n'; | |
| commentBody += '- Additional validation errors not caught by automated checks\n'; | |
| commentBody += '- Best practices and improvements\n\n'; | |
| } | |
| // Add helpful links | |
| commentBody += '---\n\n'; | |
| commentBody += 'π **Resources:**\n'; | |
| commentBody += '- [Contribution Guidelines](https://github.com/marcelscruz/dev-resources/blob/main/CONTRIBUTING.md)\n'; | |
| commentBody += '- [Valid Categories](https://github.com/marcelscruz/dev-resources/blob/main/types/category.ts)\n\n'; | |
| commentBody += '*GitHub Copilot will review this PR and report any issues found.*'; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.pull_request.number, | |
| body: commentBody | |
| }); | |
| // Request Copilot as a reviewer (this works if Copilot review is enabled in repo settings) | |
| try { | |
| await github.rest.pulls.requestReviewers({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.payload.pull_request.number, | |
| reviewers: ['copilot'] | |
| }); | |
| console.log('Requested Copilot review'); | |
| } catch (error) { | |
| console.log('Could not request Copilot review (may need to enable in repo settings):', error.message); | |
| } | |
| extract-new-resource-links: | |
| runs-on: ubuntu-latest | |
| needs: check-auto-generated | |
| # Only run if resource files are changed and no auto-generated files were changed | |
| if: needs.check-auto-generated.outputs.has_db_changes != 'true' && needs.check-auto-generated.outputs.has_readme_changes != 'true' | |
| steps: | |
| - name: Extract and post new resource links | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const { owner, repo } = context.repo; | |
| const prNumber = pr.number; | |
| // Get list of changed files | |
| const { data: files } = await github.rest.pulls.listFiles({ | |
| owner, | |
| repo, | |
| pull_number: prNumber, | |
| }); | |
| // Filter for resource files | |
| const resourceFiles = files.filter( | |
| (file) => file.filename.startsWith('resources/') && file.filename.endsWith('.ts'), | |
| ); | |
| if (resourceFiles.length === 0) { | |
| console.log('No resource files changed in this PR.'); | |
| return; | |
| } | |
| console.log(`Found ${resourceFiles.length} modified resource file(s). Checking for new resource links...`); | |
| const newLinks = []; | |
| // Helper function to extract URLs from TypeScript content | |
| function extractUrlsFromTypeScript(content) { | |
| const urls = []; | |
| // Match url property in TypeScript objects: url: 'https://...' or url: "https://..." | |
| const urlMatches = content.matchAll(/url:\s*['"`]([^'"`]+)['"`]/g); | |
| for (const match of urlMatches) { | |
| const url = match[1]; | |
| // Only include http/https URLs | |
| if (url.startsWith('http://') || url.startsWith('https://')) { | |
| urls.push(url); | |
| } | |
| } | |
| return urls; | |
| } | |
| // Check each resource file for new links | |
| for (const file of resourceFiles) { | |
| console.log(`Checking file: ${file.filename}`); | |
| let baseContent = ''; | |
| let headContent = ''; | |
| // Get base content (if file exists in base) | |
| try { | |
| const baseRes = await github.rest.repos.getContent({ | |
| owner, | |
| repo, | |
| path: file.filename, | |
| ref: pr.base.sha, | |
| }); | |
| baseContent = Buffer.from(baseRes.data.content, 'base64').toString('utf8'); | |
| } catch (error) { | |
| // File doesn't exist in base (new file) | |
| console.log(`File ${file.filename} is new in this PR`); | |
| } | |
| // Get head content | |
| try { | |
| const headRes = await github.rest.repos.getContent({ | |
| owner: pr.head.repo.owner.login, | |
| repo: pr.head.repo.name, | |
| path: file.filename, | |
| ref: pr.head.sha, | |
| }); | |
| headContent = Buffer.from(headRes.data.content, 'base64').toString('utf8'); | |
| } catch (error) { | |
| console.log(`Could not get head content for ${file.filename}:`, error.message); | |
| continue; | |
| } | |
| // Extract URLs from TypeScript resource files | |
| const baseUrls = new Set(extractUrlsFromTypeScript(baseContent)); | |
| const headUrls = new Set(extractUrlsFromTypeScript(headContent)); | |
| const fileNewLinks = [...headUrls].filter((url) => !baseUrls.has(url)); | |
| newLinks.push(...fileNewLinks); | |
| console.log( | |
| `File ${file.filename}: Base URLs: ${baseUrls.size}, Head URLs: ${headUrls.size}, New: ${fileNewLinks.length}`, | |
| ); | |
| } | |
| console.log(`Total new resource links found: ${newLinks.length}`); | |
| if (newLinks.length > 0) { | |
| console.log('New links:', newLinks); | |
| // Post comment with new links | |
| const linkComment = | |
| newLinks.length === 1 | |
| ? `**New resource link:** ${newLinks[0]}` | |
| : ['**New resource links:**', '', ...newLinks.map((link) => `- ${link}`)].join('\n'); | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| body: linkComment, | |
| }); | |
| console.log('Comment posted with new resource links.'); | |
| } else { | |
| console.log('No new resource links found in this PR.'); | |
| } |