diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d8b278..065cc31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,9 +97,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f - - name: Lowercase repository name - run: echo "REPO=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} - - name: Build Docker image (load locally for scanning) uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 with: @@ -107,8 +104,8 @@ jobs: load: true pull: true tags: | - ghcr.io/${{ env.REPO }}/systemcraft-web:latest - ghcr.io/${{ env.REPO }}/systemcraft-web:${{ github.sha }} + ghcr.io/${{ github.repository }}/systemcraft-web:latest + ghcr.io/${{ github.repository }}/systemcraft-web:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | @@ -123,7 +120,7 @@ jobs: - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 with: - image-ref: 'ghcr.io/${{ env.REPO }}/systemcraft-web:${{ github.sha }}' + image-ref: 'ghcr.io/${{ github.repository }}/systemcraft-web:${{ github.sha }}' format: 'table' exit-code: '1' ignore-unfixed: true @@ -140,7 +137,7 @@ jobs: run: | docker run -d --name systemcraft-e2e --network e2e-network -p 3000:3000 \ -e REDIS_URL=redis://redis-e2e:6379 \ - ghcr.io/${{ env.REPO }}/systemcraft-web:${{ github.sha }} + ghcr.io/${{ github.repository }}/systemcraft-web:${{ github.sha }} # Wait for container to be ready echo "Waiting for container to be ready on port 3000..." timeout 60 bash -c 'until curl -fsS http://localhost:3000 > /dev/null 2>&1; do sleep 2; done' @@ -173,8 +170,8 @@ jobs: - name: Push Docker image if: github.event_name != 'pull_request' run: | - docker push ghcr.io/${{ env.REPO }}/systemcraft-web:latest - docker push ghcr.io/${{ env.REPO }}/systemcraft-web:${{ github.sha }} + docker push ghcr.io/${{ github.repository }}/systemcraft-web:latest + docker push ghcr.io/${{ github.repository }}/systemcraft-web:${{ github.sha }} deploy: name: Deploy to Production diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml new file mode 100644 index 0000000..9c8b907 --- /dev/null +++ b/.github/workflows/issue-labeler.yml @@ -0,0 +1,80 @@ +name: Auto-label Issues + +on: + issues: + types: [opened, edited] + +permissions: + issues: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + - name: Apply labels based on title and body + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const title = (context.payload.issue.title || '').toLowerCase(); + const body = (context.payload.issue.body || '').toLowerCase(); + const text = `${title} ${body}`; + + const labelsToAdd = new Set(); + + // bug + if (/\b(bug|fix|broken|error|crash|fail|not working|regression)\b/.test(text)) { + labelsToAdd.add('bug'); + } + + // enhancement / feature + if (/\b(feat|feature|add|enhance|improve|new|request)\b/.test(text)) { + labelsToAdd.add('enhancement'); + } + + // documentation + if (/\b(docs?|documentation|readme|guide|comment|typo)\b/.test(text)) { + labelsToAdd.add('documentation'); + } + + // interview / chaos + if (/\b(interview|chaos|constraint|difficulty|easy|medium|hard|timer|hint|scoring|rubric)\b/.test(text)) { + labelsToAdd.add('interview'); + } + + // ui + if (/\b(ui|ux|design|layout|style|css|skeleton|animation|visual|loading|dark mode|responsive)\b/.test(text)) { + labelsToAdd.add('ui'); + } + + // performance + if (/\b(performance|perf|slow|speed|latency|memory|optimi[sz]e|bundle|cache)\b/.test(text)) { + labelsToAdd.add('performance'); + } + + // security + if (/\b(security|auth|token|xss|injection|vulnerability|cve|exploit)\b/.test(text)) { + labelsToAdd.add('security'); + } + + // architecture + if (/\b(architecture|canvas|node|connection|graph|topology|system design)\b/.test(text)) { + labelsToAdd.add('Architecture'); + } + + // question + if (/\b(question|how|why|what|unclear|confused|help)\b/.test(text) || title.endsWith('?')) { + labelsToAdd.add('question'); + } + + // always add needs-triage + labelsToAdd.add('needs-triage'); + + if (labelsToAdd.size === 0) return; + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + labels: [...labelsToAdd], + }); diff --git a/.github/workflows/pr-review-bot.yml b/.github/workflows/pr-review-bot.yml new file mode 100644 index 0000000..9d1c3aa --- /dev/null +++ b/.github/workflows/pr-review-bot.yml @@ -0,0 +1,110 @@ +name: PR Review Bot + +on: + pull_request: + types: [opened] + +permissions: + pull-requests: write + +jobs: + comment: + runs-on: ubuntu-latest + steps: + - name: Post PR checklist comment + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }); + + const branch = pr.head.ref.toLowerCase(); + const title = pr.title.toLowerCase(); + + // Detect type from branch name or PR title + const isFix = /^fix\/|^hotfix\/|\bfix\b|\bbug\b/.test(branch + title); + const isFeat = /^feat\/|^feature\/|\bfeat\b|\bfeature\b/.test(branch + title); + const isInterview = /interview|chaos|constraint|scoring/.test(branch + title); + const isUI = /ui|ux|skeleton|style|design|layout/.test(branch + title); + + let typeNote = ''; + if (isFeat) typeNote = '> This looks like a **feature PR**. Make sure any new behaviour is covered by tests.\n\n'; + else if (isFix) typeNote = '> This looks like a **bug fix**. A regression test that reproduces the original issue would be ideal.\n\n'; + + let interviewNote = ''; + if (isInterview) { + interviewNote = ` + ### Interview / Chaos mode checklist + - [ ] Chaos triggers correctly for all difficulties (easy, medium, hard) + - [ ] Interviewer chat messages are urgent when chaos is active + - [ ] Scoring reflects partial credit where applicable + - [ ] \`constraintTrigger.test.ts\` passes + `; + } + + let uiNote = ''; + if (isUI) { + uiNote = ` + ### UI checklist + - [ ] Skeleton loaders in place for any new loading states + - [ ] Dark mode tested + - [ ] No layout shift on load + `; + } + + const body = `## 👋 Thanks for the PR! + + ${typeNote}Please run through this checklist before requesting a review: + + ### Core + - [ ] Linked the related issue below (use \`Closes #\` or \`Fixes #\`) + - [ ] \`npm run lint\` passes with no errors + - [ ] \`npm run typecheck\` (or \`tsc --noEmit\`) passes + - [ ] Existing tests still pass (\`npm test\`) + - [ ] New behaviour has a test (or explain why one isn't needed) + + ### Docs / changelog + - [ ] Updated any affected inline comments or JSDoc if the API changed + - [ ] If a public-facing behaviour changed, noted it in the PR description + ${interviewNote}${uiNote} + --- + _This comment is posted automatically by \`github-actions[bot]\`. Tick the boxes as you go — reviewers will use this as a quick sanity check._`; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body, + }); + + - name: Auto-label PR from branch name + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const branch = context.payload.pull_request.head.ref.toLowerCase(); + const title = (context.payload.pull_request.title || '').toLowerCase(); + const text = `${branch} ${title}`; + + const labels = new Set(); + + if (/^fix\/|^hotfix\/|\bfix\b|\bbug\b/.test(text)) labels.add('bug'); + if (/^feat\/|^feature\/|\bfeat\b|\bfeature\b/.test(text)) labels.add('enhancement'); + if (/^docs?\/|\bdoc\b|\bdocs\b|\bdocumentation\b/.test(text)) labels.add('documentation'); + if (/interview|chaos|constraint|scoring|difficulty/.test(text)) labels.add('interview'); + if (/ui|ux|skeleton|style|design|layout|visual/.test(text)) labels.add('ui'); + if (/perf|performance|optim|speed|bundle/.test(text)) labels.add('performance'); + if (/security|auth|token|vuln/.test(text)) labels.add('security'); + + if (labels.size === 0) return; + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: [...labels], + });