diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ef373d9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ +## Description + + +## Related Issue or Discussion + + + + +## Type of Change +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Documentation update + +## Checklist +- [ ] I have read the [Contributing Guidelines](https://github.com/mauriceboe/TREK/wiki/Contributing) +- [ ] My branch is [up to date with `dev`](https://github.com/mauriceboe/TREK/wiki/Development-environment#3-keep-your-fork-up-to-date) +- [ ] This PR targets the `dev` branch, not `main` +- [ ] I have tested my changes locally +- [ ] I have added/updated tests that prove my fix is effective or that my feature works +- [ ] I have updated documentation if needed diff --git a/.github/workflows/close-stale-invalid-titles.yml b/.github/workflows/close-stale-invalid-titles.yml new file mode 100644 index 0000000..fa9fff4 --- /dev/null +++ b/.github/workflows/close-stale-invalid-titles.yml @@ -0,0 +1,71 @@ +name: Close issues with unchanged bad titles + +on: + schedule: + - cron: '0 */6 * * *' # Every 6 hours + +permissions: + issues: write + +jobs: + close-stale: + runs-on: ubuntu-latest + steps: + - name: Close stale invalid-title issues + uses: actions/github-script@v7 + with: + script: | + const badTitles = [ + "[bug]", "bug report", "bug", "issue", + "help", "question", "test", "...", "untitled" + ]; + + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: 'invalid-title', + state: 'open', + per_page: 100, + }); + + const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + + for (const issue of issues) { + const createdAt = new Date(issue.created_at); + if (createdAt > twentyFourHoursAgo) continue; // grace period not over yet + + const titleLower = issue.title.trim().toLowerCase(); + + if (!badTitles.includes(titleLower)) { + // Title was fixed — remove the label and move on + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: 'invalid-title', + }); + continue; + } + + // Still a bad title after 24h — close it + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: [ + '## Issue closed', + '', + 'This issue has been automatically closed because the title was not updated within 24 hours.', + '', + 'Feel free to open a new issue with a descriptive title that summarizes the problem.', + ].join('\n'), + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed', + state_reason: 'not_planned', + }); + } diff --git a/.github/workflows/close-stale-wrong-branch.yml b/.github/workflows/close-stale-wrong-branch.yml new file mode 100644 index 0000000..10b9cd1 --- /dev/null +++ b/.github/workflows/close-stale-wrong-branch.yml @@ -0,0 +1,66 @@ +name: Close PRs with unchanged wrong base branch + +on: + schedule: + - cron: '0 */6 * * *' # Every 6 hours + +permissions: + pull-requests: write + issues: write + +jobs: + close-stale: + runs-on: ubuntu-latest + steps: + - name: Close stale wrong-base-branch PRs + uses: actions/github-script@v7 + with: + script: | + const { data: pulls } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100, + }); + + const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + + for (const pull of pulls) { + const hasLabel = pull.labels.some(l => l.name === 'wrong-base-branch'); + if (!hasLabel) continue; + + const createdAt = new Date(pull.created_at); + if (createdAt > twentyFourHoursAgo) continue; // grace period not over yet + + // Base was fixed — remove label and move on + if (pull.base.ref !== 'main') { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pull.number, + name: 'wrong-base-branch', + }); + continue; + } + + // Still targeting main after 24h — close it + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pull.number, + body: [ + '## PR closed', + '', + 'This PR has been automatically closed because the base branch was not updated to `dev` within 24 hours.', + '', + 'Feel free to open a new PR targeting `dev`.', + ].join('\n'), + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pull.number, + state: 'closed', + }); + } diff --git a/.github/workflows/close-untitled-issues.yml b/.github/workflows/close-untitled-issues.yml index 3495bdf..a9a94e9 100644 --- a/.github/workflows/close-untitled-issues.yml +++ b/.github/workflows/close-untitled-issues.yml @@ -1,4 +1,4 @@ -name: Close untitled issues +name: Flag issues with bad titles on: issues: @@ -10,58 +10,83 @@ permissions: jobs: check-title: runs-on: ubuntu-latest - permissions: - issues: write steps: - - name: Close if title is empty or generic + - name: Flag or redirect issue uses: actions/github-script@v7 with: script: | const title = context.payload.issue.title.trim(); - const badTitles = [ - "[bug]", - "bug report", - "bug", - "issue", - ]; - - const featureRequestTitles = [ - "feature request", - "[feature]", - "[feature request]", - "[enhancement]" - ] - const titleLower = title.toLowerCase(); + const badTitles = [ + "[bug]", "bug report", "bug", "issue", + "help", "question", "test", "...", "untitled" + ]; + + const featureRequestTitles = [ + "feature request", "[feature]", "[feature request]", "[enhancement]" + ]; + if (badTitles.includes(titleLower)) { + // Ensure the label exists + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'invalid-title', + }); + } catch { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'invalid-title', + color: 'e4e669', + description: 'Issue title does not meet quality standards', + }); + } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + labels: ['invalid-title'], + }); + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, - body: "This issue was closed because no title was provided. Please re-open with a descriptive title that summarizes the problem." + body: [ + '## Invalid title', + '', + `Your issue title \`${title}\` is too generic to be actionable.`, + '', + 'Please edit the title to something descriptive that summarizes the problem — for example:', + '> _Map view crashes when zooming in on Safari 17_', + '', + '**This issue will be automatically closed in 24 hours if the title has not been updated.**', + ].join('\n'), }); - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.issue.number, - state: "closed", - state_reason: "not_planned" - }); } else if (featureRequestTitles.some(t => titleLower.startsWith(t))) { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, - body: "Feature requests should be made in the [Discussions](https://github.com/mauriceboe/TREK/discussions/new?category=feature-requests) — not as issues. This issue has been closed." + body: [ + '## Wrong place for feature requests', + '', + 'Feature requests should be submitted in [Discussions](https://github.com/mauriceboe/TREK/discussions/new?category=feature-requests), not as issues.', + '', + 'This issue has been closed. Feel free to re-submit your idea in the right place!', + ].join('\n'), }); await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, - state: "closed", - state_reason: "not_planned" + state: 'closed', + state_reason: 'not_planned', }); - } \ No newline at end of file + } diff --git a/.github/workflows/enforce-target-branch.yml b/.github/workflows/enforce-target-branch.yml new file mode 100644 index 0000000..d8027cc --- /dev/null +++ b/.github/workflows/enforce-target-branch.yml @@ -0,0 +1,100 @@ +name: Enforce PR Target Branch + +on: + pull_request: + types: [opened, reopened, edited, synchronize] + +jobs: + check-target: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Flag or clear wrong base branch + uses: actions/github-script@v7 + with: + script: | + const base = context.payload.pull_request.base.ref; + const labels = context.payload.pull_request.labels.map(l => l.name); + const prNumber = context.payload.pull_request.number; + + // If the base was fixed, remove the label and let it through + if (base !== 'main') { + if (labels.includes('wrong-base-branch')) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: 'wrong-base-branch', + }); + } + return; + } + + // Base is main — check if this user is a maintainer + let permission = 'none'; + try { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.payload.pull_request.user.login, + }); + permission = data.permission; + } catch (_) { + // User is not a collaborator — treat as 'none' + } + + if (['admin', 'write'].includes(permission)) { + console.log(`User has '${permission}' permission, skipping.`); + return; + } + + // Already labeled — avoid spamming on every push + if (labels.includes('wrong-base-branch')) { + core.setFailed("PR must target `dev`, not `main`."); + return; + } + + // Ensure the label exists + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'wrong-base-branch', + }); + } catch { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'wrong-base-branch', + color: 'd73a4a', + description: 'PR is targeting the wrong base branch', + }); + } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: ['wrong-base-branch'], + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: [ + '## Wrong target branch', + '', + 'This PR targets `main`, but contributions must go through `dev` first.', + '', + 'To fix this, click **Edit** next to the PR title and change the base branch to `dev`.', + '', + '**This PR will be automatically closed in 24 hours if the base branch has not been updated.**', + '', + '> _If you need to merge directly to `main`, contact a maintainer._', + ].join('\n'), + }); + + core.setFailed("PR must target `dev`, not `main`."); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 684c775..d4743d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,6 +9,8 @@ Thanks for your interest in contributing! Please read these guidelines before op 3. **No breaking changes** — Backwards compatibility is non-negotiable 4. **Target the `dev` branch** — All PRs must be opened against `dev`, not `main` 5. **Match the existing style** — No reformatting, no linter config changes, no "while I'm here" cleanups +6. **Tests** — Your changes must include tests. The project maintains 80%+ coverage; PRs that drop it will be closed +7. **Branch up to date** — Your branch must be [up to date with `dev`](https://github.com/mauriceboe/TREK/wiki/Development-environment#3-keep-your-fork-up-to-date) before submitting a PR ## Pull Requests @@ -35,22 +37,9 @@ fix(maps): correct zoom level on Safari feat(budget): add CSV export for expenses ``` -## Development Setup +## Development Environment -```bash -git clone https://github.com/mauriceboe/TREK.git -cd TREK - -# Server -cd server && npm install && npm run dev - -# Client (separate terminal) -cd client && npm install && npm run dev -``` - -Server: `http://localhost:3001` | Client: `http://localhost:5173` - -On first run, check the server logs for the auto-generated admin credentials. +See the [Developer Environment page](https://github.com/mauriceboe/TREK/wiki/Development-environment) for more information on setting up your development environment. ## More Details