From cad5344d410a025e17d4acfa5afd5a9e3a67b36e Mon Sep 17 00:00:00 2001 From: akhileshrangani4 Date: Thu, 12 Mar 2026 10:26:05 -0700 Subject: [PATCH 1/3] ci: add CI, conventional commits, and auto-labeling workflows Add GitHub Actions workflows for: - CI pipeline (lint, test, build) on PRs and pushes to main - PR title validation for conventional commit format - Automatic area and change type labeling on PRs/issues Also fix outdated model ID in ai provider test. --- .github/labels.yml | 140 ++++++++++++ .github/workflows/automation-labels.yml | 243 +++++++++++++++++++++ .github/workflows/ci.yml | 65 ++++++ .github/workflows/conventional-commits.yml | 27 +++ ai/src/providers/index.test.ts | 2 +- 5 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 .github/labels.yml create mode 100644 .github/workflows/automation-labels.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/conventional-commits.yml diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..33b8f8b --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,140 @@ +# Area labels - which part of the codebase +- name: "area: web" + color: "26b5ce" + description: "Changes to the web app" + +- name: "area: server" + color: "26b5ce" + description: "Changes to the server" + +- name: "area: db" + color: "26b5ce" + description: "Changes to the database package" + +- name: "area: ai" + color: "26b5ce" + description: "Changes to the AI package" + +- name: "area: lib" + color: "26b5ce" + description: "Changes to the shared lib" + +- name: "area: packages" + color: "26b5ce" + description: "Changes to internal packages" + +- name: "area: templates" + color: "26b5ce" + description: "Changes to templates" + +- name: "area: tests" + color: "26b5ce" + description: "Changes to tests" + +- name: "area: config" + color: "26b5ce" + description: "Changes to repository configuration files" + +- name: "area: documentation" + color: "26b5ce" + description: "Improvements or additions to documentation" + +- name: "area: github actions" + color: "26b5ce" + description: "Changes to GitHub Actions workflows" + +# Change labels - conventional commit type +- name: "change: feat" + color: "32e52f" + description: "New feature" + +- name: "change: fix" + color: "158c01" + description: "Bug fix" + +- name: "change: chore" + color: "ededed" + description: "Maintenance and chores" + +- name: "change: docs" + color: "0075ca" + description: "Documentation changes" + +- name: "change: refactor" + color: "fbca04" + description: "Code refactoring" + +- name: "change: test" + color: "bfd4f2" + description: "Test changes" + +- name: "change: ci" + color: "000000" + description: "CI/CD changes" + +- name: "change: perf" + color: "f9d0c4" + description: "Performance improvements" + +- name: "change: build" + color: "5319e7" + description: "Build system changes" + +- name: "change: style" + color: "c5def5" + description: "Code style changes" + +- name: "change: revert" + color: "d93f0b" + description: "Reverting changes" + +- name: "change: deps" + color: "0366d6" + description: "Dependency updates" + +# Status labels +- name: "status: do not merge" + color: "e11d21" + description: "PR should not be merged" + +- name: "status: duplicate" + color: "cfd3d7" + description: "This issue or pull request already exists" + +- name: "status: in progress" + color: "cccccc" + description: "Work is currently being done" + +- name: "status: triage" + color: "fef2c0" + description: "Needs to be triaged by a maintainer" + +- name: "status: blocked" + color: "e11d21" + description: "This issue or pull request is blocked" + +- name: "status: ready" + color: "339E62" + description: "Ready for review" + +# Type labels +- name: "type: bug" + color: "d73a4a" + description: "Something isn't working" + +- name: "type: enhancement" + color: "a2eeef" + description: "New feature or request" + +# Contributor labels +- name: "contributor: bot" + color: "0366d6" + description: "Created by a bot" + +- name: "contributor: team" + color: "80ffce" + description: "Created by a team member" + +- name: "contributor: external" + color: "6f42c1" + description: "Created by an external contributor" diff --git a/.github/workflows/automation-labels.yml b/.github/workflows/automation-labels.yml new file mode 100644 index 0000000..9dc83a5 --- /dev/null +++ b/.github/workflows/automation-labels.yml @@ -0,0 +1,243 @@ +name: Automation / Labels + +# SECURITY NOTE: This workflow uses pull_request_target to gain write permissions +# for labeling PRs from forks. This is safe because: +# 1. We do NOT checkout fork code +# 2. We use GitHub API for file detection +# 3. We only read PR metadata - never execute PR code + +on: + pull_request_target: + types: [opened, synchronize, reopened, edited, labeled, unlabeled] + + issues: + types: [opened] + + push: + branches: + - "main" + paths: + - ".github/labels.yml" + - ".github/workflows/automation-labels.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.ref }} + cancel-in-progress: true + +jobs: + meta: + name: "Meta" + permissions: + contents: read + runs-on: ubuntu-latest + outputs: + areas: ${{ steps.files_changed.outputs.areas || '[]' }} + change_type: ${{ steps.change_type.outputs.change_type }} + + steps: + - name: Determine Change Type + id: change_type + if: github.event_name == 'pull_request_target' + env: + PR_TITLE: "${{ github.event.pull_request.title }}" + run: | + CHANGE_TYPE="" + if [[ "${PR_TITLE}" =~ ^(feat|fix|chore|docs|refactor|test|ci|perf|build|style|revert|deps)(\(.+\))?!?:.*$ ]]; then + CHANGE_TYPE="${BASH_REMATCH[1]}" + fi + echo "change_type=${CHANGE_TYPE}" >> "${GITHUB_OUTPUT}" + + - name: Determine Areas via GitHub API + id: files_changed + if: github.event_name == 'pull_request_target' || github.event_name == 'push' + uses: actions/github-script@v7 + with: + script: | + const changedPathSet = new Set(); + + if (context.eventName === 'push') { + if (context.payload.before && context.payload.after) { + const { data: compareData } = await github.rest.repos.compareCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + base: context.payload.before, + head: context.payload.after + }); + for (const file of compareData.files || []) { + changedPathSet.add(file.filename); + } + } + } else if (context.eventName === 'pull_request_target') { + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + per_page: 100 + }); + for (const file of files) { + changedPathSet.add(file.filename); + } + } + + const changedPaths = Array.from(changedPathSet); + + const areaPatterns = { + web: [/^web\//], + server: [/^server\//], + db: [/^db\//], + ai: [/^ai\//], + lib: [/^lib\//], + packages: [/^packages\//], + templates: [/^templates\//], + tests: [/^tests\//], + config: [ + /^turbo\.json$/, + /^\.nvmrc$/, + /^\.node-version$/, + /^\.prettier/, + /\.config\.(js|cjs|mjs|ts|cts|mts|json)$/ + ], + documentation: [ + /\.mdx?$/, + /README\./i + ], + github_actions: [ + /^\.github\/workflows\//, + /^\.github\/actions\// + ], + labels: [ + /^\.github\/labels\.yml$/, + /^\.github\/workflows\/automation-labels\.yml$/ + ], + }; + + const areas = []; + for (const [area, patterns] of Object.entries(areaPatterns)) { + if (changedPaths.some(path => patterns.some(pattern => pattern.test(path)))) { + areas.push(area); + } + } + + core.setOutput('areas', JSON.stringify(areas)); + + repo-labels: + name: "Repository Labels" + runs-on: ubuntu-latest + needs: [meta] + if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target' }} + permissions: + contents: read + issues: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Update Labels + uses: crazy-max/ghaction-github-labeler@v5 + if: ${{ contains(fromJson(needs.meta.outputs.areas || '[]'), 'labels') }} + with: + dry-run: ${{ github.event_name == 'pull_request_target' }} + github-token: ${{ secrets.GITHUB_TOKEN }} + skip-delete: true + yaml-file: .github/labels.yml + + do-not-merge: + name: "Do Not Merge" + runs-on: ubuntu-latest + if: github.event_name == 'pull_request_target' + permissions: + contents: read + steps: + - name: Check + env: + DO_NOT_MERGE: "${{contains(github.event.pull_request.labels.*.name, 'status: do not merge')}}" + run: | + if [[ "${DO_NOT_MERGE}" == "true" ]]; then + echo "##[error]Cannot merge when 'status: do not merge' label is present. Remove the label to proceed." + exit 1 + else + echo "No 'status: do not merge' label found. Passing check." + fi + + sync-labels: + name: "Sync Labels" + needs: [meta, repo-labels] + runs-on: ubuntu-latest + if: (github.event_name == 'pull_request_target' || github.event_name == 'issues') && (github.event.action != 'labeled' && github.event.action != 'unlabeled') + permissions: + contents: read + issues: write + pull-requests: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Determine Labels + id: determine_labels + env: + EXISTING_LABELS: "${{ toJson(github.event.issue.labels || github.event.pull_request.labels || '[]') }}" + DATA_AREAS: "${{ needs.meta.outputs.areas }}" + DATA_CHANGE: "${{ needs.meta.outputs.change_type }}" + run: | + LABEL_CHANGES="{}" + + remove_label() { + local LABEL_TO_REMOVE="$1" + LABEL_CHANGES=$(jq --arg key "$LABEL_TO_REMOVE" '. + {($key): false}' <<< "${LABEL_CHANGES}") + } + + add_label() { + local LABEL_TO_ADD="$1" + LABEL_CHANGES=$(jq --arg key "$LABEL_TO_ADD" '. + {($key): true}' <<< "${LABEL_CHANGES}") + } + + remove_all_by_prefix() { + local PREFIX="$1" + while IFS= read -r LABEL; do + if [[ "$LABEL" == ${PREFIX}* ]]; then + remove_label "$LABEL" + fi + done < <(jq -r '.[] | .name' <<< "${EXISTING_LABELS}") + } + + # Areas + remove_all_by_prefix "area: " + for AREA in $(jq -r '.[]' <<< "${DATA_AREAS}"); do + if [[ "${AREA}" == "labels" ]]; then continue; fi + LABEL_KEY="area: $(echo "${AREA}" | sed 's/_/ /g')" + add_label "$LABEL_KEY" + done + + # Change Type + if [[ -n "${DATA_CHANGE}" ]]; then + remove_all_by_prefix "change: " + LABEL_KEY="change: ${DATA_CHANGE}" + add_label "$LABEL_KEY" + fi + + echo "added_labels=$(jq -c '.' <<< "${LABEL_CHANGES}")" >> "${GITHUB_OUTPUT}" + + - name: Apply Label Changes + env: + PR_OR_ISSUE_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event.issue.number }} + GH_SUBCOMMAND: ${{ github.event_name == 'pull_request_target' && 'pr' || 'issue' }} + GH_REPO: ${{ github.repository }} + LABEL_CHANGES: |- + ${{ steps.determine_labels.outputs.added_labels || '{}' }} + run: | + LABELS_TO_ADD=$(jq -r '[to_entries[] | select(.value == true) | .key] | join(",")' <<< "${LABEL_CHANGES}") + LABELS_TO_REMOVE=$(jq -r '[to_entries[] | select(.value == false) | .key] | join(",")' <<< "${LABEL_CHANGES}") + + CMD_ARGS=() + if [[ -n "${LABELS_TO_ADD}" ]]; then + CMD_ARGS+=(--add-label "${LABELS_TO_ADD}") + fi + if [[ -n "${LABELS_TO_REMOVE}" ]]; then + CMD_ARGS+=(--remove-label "${LABELS_TO_REMOVE}") + fi + + if [[ ${#CMD_ARGS[@]} -gt 0 ]]; then + gh "${GH_SUBCOMMAND}" edit "${PR_OR_ISSUE_NUMBER}" -R "${GH_REPO}" "${CMD_ARGS[@]}" 2>&1 + else + echo "No label changes to apply" + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..26403e7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: "package.json" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run Lint + run: npm run lint + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: "package.json" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run Tests + run: npm run test + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: "package.json" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + env: + SKIP_ENV_VALIDATION: "true" diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml new file mode 100644 index 0000000..98b0eef --- /dev/null +++ b/.github/workflows/conventional-commits.yml @@ -0,0 +1,27 @@ +name: PR Conventional Commit Validation + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + validate-pr-title: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + checks: write + steps: + - name: PR Conventional Commit Validation + uses: ytanikin/pr-conventional-commits@1.5.1 + with: + task_types: '["feat","fix","perf","deps","revert","docs","style","chore","refactor","test","build","ci"]' + add_label: "false" diff --git a/ai/src/providers/index.test.ts b/ai/src/providers/index.test.ts index 8789804..ff8f4f7 100644 --- a/ai/src/providers/index.test.ts +++ b/ai/src/providers/index.test.ts @@ -29,7 +29,7 @@ describe("resolveProviderConfig", () => { it("defaults to anthropic when no overrides and no env vars", () => { const config = resolveProviderConfig() expect(config.provider).toBe("anthropic") - expect(config.modelId).toBe("claude-sonnet-4-20250514") + expect(config.modelId).toBe("claude-sonnet-4-6") }) it("detects anthropic from env", () => { From 6549e57e51501e2b2de890592c3cd30a39429f22 Mon Sep 17 00:00:00 2001 From: akhileshrangani4 Date: Thu, 12 Mar 2026 10:29:38 -0700 Subject: [PATCH 2/3] ci: separate e2e tests from unit tests Rename tests/ workspace script from "test" to "test:e2e" so turbo test only runs unit tests that can pass without a live server. Add a manual workflow_dispatch trigger for running e2e tests in CI with secrets. --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ package.json | 1 + tests/package.json | 2 +- turbo.json | 4 ++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26403e7..3cb75e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,12 @@ on: branches: [main] pull_request: branches: [main] + workflow_dispatch: + inputs: + run_e2e: + description: "Run E2E integration tests" + type: boolean + default: false concurrency: group: ${{ github.workflow }}-${{ github.ref_name }} @@ -63,3 +69,24 @@ jobs: run: npm run build env: SKIP_ENV_VALIDATION: "true" + + e2e: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_e2e }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: "package.json" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run E2E Tests + run: npm run test:e2e + env: + CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} + CLERK_TEST_USER_ID: ${{ secrets.CLERK_TEST_USER_ID }} + GITHUB_PAT: ${{ secrets.E2E_GITHUB_PAT }} diff --git a/package.json b/package.json index 95e92e6..0fe79e5 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dev": "turbo dev", "lint": "turbo lint", "test": "turbo test", + "test:e2e": "turbo test:e2e", "db:migrate": "npm run migrate -w db", "db:migrate:prod": "NODE_ENV=production npm run migrate -w db", "db:generate": "npm run generate -w db", diff --git a/tests/package.json b/tests/package.json index 62a0866..8b8d97b 100644 --- a/tests/package.json +++ b/tests/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "vitest dev --testTimeout=30000", "dev:hi": "vitest dev github.test.ts --testTimeout=30000", - "test": "vitest run" + "test:e2e": "vitest run" }, "author": "Your Name", "license": "ISC", diff --git a/turbo.json b/turbo.json index 54281ec..d761c00 100644 --- a/turbo.json +++ b/turbo.json @@ -44,6 +44,10 @@ }, "test": { "dependsOn": ["^build"] + }, + "test:e2e": { + "dependsOn": ["^build"], + "cache": false } } } From f881088dbd5415f00664fb370296d5c99ecd1d40 Mon Sep 17 00:00:00 2001 From: akhileshrangani4 Date: Thu, 12 Mar 2026 10:34:31 -0700 Subject: [PATCH 3/3] fix: wire up SKIP_ENV_VALIDATION in web env config The t3-oss env validation was ignoring SKIP_ENV_VALIDATION because skipValidation wasn't passed to createEnv. This broke CI builds that don't have secrets available. --- .github/workflows/ci.yml | 7 +++++++ web/lib/env.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cb75e3..ba7f709 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,13 @@ jobs: run: npm run build env: SKIP_ENV_VALIDATION: "true" + GITHUB_CLIENT_ID: "dummy" + GITHUB_CLIENT_SECRET: "dummy" + E2B_API_KEY: "dummy" + CLERK_SECRET_KEY: "sk_test_dummy" + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "pk_test_dGVuZGVyLWhvbmV5YmVlLTU2LmNsZXJrLmFjY291bnRzLmRldiQ" + NEXT_PUBLIC_APP_URL: "http://localhost:3000" + DATABASE_URL: "postgresql://dummy:dummy@localhost:5432/dummy" e2e: runs-on: ubuntu-latest diff --git a/web/lib/env.ts b/web/lib/env.ts index 4a3b7ac..153dc21 100644 --- a/web/lib/env.ts +++ b/web/lib/env.ts @@ -2,6 +2,7 @@ import { createEnv } from "@t3-oss/env-nextjs" import { z } from "zod" export const env = createEnv({ + skipValidation: process.env.SKIP_ENV_VALIDATION === "true", server: { GITHUB_CLIENT_ID: z.string().min(1), GITHUB_CLIENT_SECRET: z.string().min(1),