name: issue-format on: issues: types: [opened, edited] permissions: issues: write jobs: validate: runs-on: ubuntu-latest # Skip issues opened by bots (e.g. dependabot) and maintainers fixing format if: >- github.event.issue.user.type != 'Bot' && !contains(github.event.issue.labels.*.name, 'format-exempt') steps: - name: Validate title and body uses: actions/github-script@v7 with: script: | const issue = context.payload.issue; const title = issue.title.trim(); const body = (issue.body || '').trim(); const errors = []; const modernTitle = /^\[(Bug|Feature)\] .{8,}/.test(title); const legacyAppTitle = /^Bug:\s*.{5,}/i.test(title); if (!modernTitle && !legacyAppTitle) { errors.push( 'Title must start with `[Bug]` or `[Feature]` followed by a short summary (at least 8 characters after the prefix). Legacy app links using `Bug: ...` are also accepted. Example: `[Bug] SFTP upload fails on Windows`' ); } if (body.length < 120) { errors.push( 'Body is too short. Please use the Bug Report or Feature Request template and fill in all required fields.' ); } const templateMarkers = [ 'Steps to reproduce', 'Expected behavior', 'Actual behavior', 'Describe the problem', 'Problem / pain point', 'Proposed solution', 'Operating system', ]; const hasTemplateStructure = templateMarkers.some((marker) => body.includes(marker) ); if (!hasTemplateStructure) { errors.push( 'Body does not look like it came from an issue template. Choose **Bug Report** or **Feature Request** when opening an issue.' ); } const labels = new Set( (issue.labels || []).map((label) => typeof label === 'string' ? label : label.name ) ); if (errors.length === 0) { if ( issue.state === 'closed' && labels.has('invalid-format') ) { labels.delete('invalid-format'); await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, state: 'open', labels: [...labels], }); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, body: ' Format looks good now. Reopening this issue.', }); } core.info('Issue format OK'); return; } const issueNumber = issue.number; const marker = ''; const bodyText = [ marker, '## Issue format check failed', '', 'This issue was closed automatically because it does not follow the required format.', '', ...errors.map((e) => `- ${e}`), '', '### How to resubmit', '', '1. Go to [New Issue](https://github.com/binaricat/Netcatty/issues/new/choose)', '2. Pick **Bug Report** or **Feature Request**', '3. Fill in every required field', '4. Keep the `[Bug]` or `[Feature]` prefix in the title and add a clear summary after it (older app versions may use `Bug: ...`)', '', 'For questions and open-ended discussion, use [GitHub Discussions](https://github.com/binaricat/Netcatty/discussions) instead.', '', 'If you believe this was a mistake, reply here after fixing the title/body and a maintainer can reopen.', ].join('\n'); const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, per_page: 100, }); const alreadyNotified = comments.some((c) => (c.body || '').includes(marker) ); if (!alreadyNotified) { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, body: bodyText, }); } labels.add('invalid-format'); await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, state: 'closed', state_reason: 'not_planned', labels: [...labels], });