Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,15 @@ 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:
context: .
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: |
Expand All @@ -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
Expand All @@ -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'
Expand Down Expand Up @@ -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
Expand Down
80 changes: 80 additions & 0 deletions .github/workflows/issue-labeler.yml
Original file line number Diff line number Diff line change
@@ -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],
});
110 changes: 110 additions & 0 deletions .github/workflows/pr-review-bot.yml
Original file line number Diff line number Diff line change
@@ -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 #<number>\` or \`Fixes #<number>\`)
- [ ] \`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],
});
Loading