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
18 changes: 18 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Setup Node and pnpm
description: Install pnpm, Node.js, and workspace dependencies with frozen lockfile
inputs:
registry-url:
description: Optional npm registry URL for actions/setup-node (used by npm publish)
required: false
default: ""
runs:
using: composite
steps:
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 26.12.1
cache: pnpm
registry-url: ${{ inputs.registry-url }}
- run: pnpm install --frozen-lockfile
shell: bash
41 changes: 41 additions & 0 deletions .github/actions/verify/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Verify
description: Setup toolchain and run CSP, typecheck, lint, unit, WAF smoke, and E2E checks
inputs:
vite-github-client-id:
description: VITE_GITHUB_CLIENT_ID for Playwright E2E tests
required: false
default: ""
deploy-domain:
description: Production domain for WAF smoke tests (skipped if empty)
required: false
default: ""
runs:
using: composite
steps:
- uses: ./.github/actions/setup
- name: Verify CSP inline script hash
shell: bash
run: bash scripts/verify-csp-hash.sh
- name: Typecheck
shell: bash
run: pnpm run typecheck
- name: Lint
shell: bash
run: pnpm lint
- name: Unit tests
shell: bash
run: pnpm test
- name: WAF smoke tests
if: inputs.deploy-domain != ''
shell: bash
run: pnpm test:waf "https://$DEPLOY_DOMAIN"
env:
DEPLOY_DOMAIN: ${{ inputs.deploy-domain }}
# No `playwright install` step: playwright.config.ts uses channel: "chrome",
# which resolves to ubuntu-latest's preinstalled Google Chrome — Playwright's
# own bundled Chromium download is never used and would be wasted work.
- name: Run E2E tests
shell: bash
run: pnpm test:e2e
env:
VITE_GITHUB_CLIENT_ID: ${{ inputs.vite-github-client-id }}
19 changes: 3 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: ./.github/actions/verify
with:
node-version: 26.12.1
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Verify CSP inline script hash
run: bash scripts/verify-csp-hash.sh
- run: pnpm run typecheck
- run: pnpm test
# No `playwright install` step: playwright.config.ts uses channel: "chrome",
# which resolves to ubuntu-latest's preinstalled Google Chrome — Playwright's
# own bundled Chromium download is never used and would be wasted work.
- name: Run E2E tests
run: pnpm test:e2e
env:
VITE_GITHUB_CLIENT_ID: ${{ vars.VITE_GITHUB_CLIENT_ID }}
vite-github-client-id: ${{ vars.VITE_GITHUB_CLIENT_ID }}
deploy-domain: ${{ vars.DEPLOY_DOMAIN }}
15 changes: 3 additions & 12 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: ./.github/actions/verify
with:
node-version: 26.12.1
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
- run: pnpm test
- name: Verify CSP inline script hash
run: bash scripts/verify-csp-hash.sh
- name: WAF smoke tests
if: vars.DEPLOY_DOMAIN != ''
run: pnpm test:waf "https://${{ vars.DEPLOY_DOMAIN }}"
vite-github-client-id: ${{ vars.VITE_GITHUB_CLIENT_ID }}
deploy-domain: ${{ vars.DEPLOY_DOMAIN }}
- name: Validate deploy configuration
run: pnpm validate:deploy
env:
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/publish-mcp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: ./.github/actions/setup
with:
node-version: 26.12.1
registry-url: "https://registry.npmjs.org"
- run: pnpm install --frozen-lockfile
- run: pnpm --filter github-tracker-mcp run typecheck
- run: pnpm --filter github-tracker-mcp run build
- run: pnpm --filter github-tracker-mcp test
Expand Down
7 changes: 1 addition & 6 deletions .github/workflows/status-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,5 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 26.12.1
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: ./.github/actions/setup
- run: pnpm test:status-smoke
83 changes: 83 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"options": {
"typeAware": true,
"denyWarnings": true,
"reportUnusedDisableDirectives": "error"
},
"plugins": ["typescript", "unicorn", "oxc", "import", "promise", "vitest"],
"categories": {
"correctness": "error",
"suspicious": "warn",
"perf": "warn"
},
"ignorePatterns": [
"dist/**",
"mcp/dist/**",
"**/node_modules/**",
".wrangler/**",
".claude/**",
"hack/**",
"playwright-report/**",
"test-results/**"
],
"rules": {
"typescript/consistent-type-imports": [
"error",
{
"disallowTypeAnnotations": false
}
],
"typescript/no-unsafe-type-assertion": "off",
"eslint/no-underscore-dangle": "off",
"unicorn/consistent-function-scoping": "off",
"import/no-unassigned-import": "off"
},
"overrides": [
{
"files": [
"src/app/services/api.ts",
"src/app/services/events.ts",
"src/app/stores/cache.ts",
"src/app/components/settings/JiraFieldPicker.tsx"
],
"rules": {
"eslint/no-await-in-loop": "off"
}
},
{
"files": [
"src/app/lib/mcp-relay.ts",
"src/app/lib/notifications.ts",
"src/app/lib/proxy.ts"
],
"rules": {
"unicorn/prefer-add-event-listener": "off"
}
},
{
"files": ["tests/**", "e2e/**", "mcp/tests/**"],
"plugins": ["typescript", "unicorn", "oxc", "import", "promise", "vitest"],
"rules": {
"eslint/no-await-in-loop": "off",
"unicorn/prefer-add-event-listener": "off",
"vitest/require-mock-type-parameters": "off",
"vitest/expect-expect": "off",
"typescript/unbound-method": "off",
"vitest/no-conditional-expect": "off"
}
},
{
"files": ["tests/**/*.steps.tsx", "tests/worker/crypto.test.ts"],
"rules": {
"vitest/no-standalone-expect": "off"
}
},
{
"files": ["tests/lib/settings-transfer.test.ts"],
"rules": {
"vitest/valid-expect": "off"
}
}
]
}
10 changes: 7 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pnpm test # unit tests (Vitest — root + mcp/)
pnpm test:e2e # Playwright E2E tests (chromium)
pnpm test:status-smoke # Live-network check that githubstatus.com still reports the tracked component names (also runs on a weekly schedule in CI)
pnpm run typecheck # TypeScript validation (root + mcp/)
pnpm lint # Type-aware lint check (oxlint — root + mcp/)
pnpm lint:fix # Auto-fix safe lint findings (oxlint)
pnpm run screenshot # Capture dashboard screenshot (saves to docs/)
pnpm mcp:serve # Start the MCP server (requires GITHUB_TOKEN)
```
Expand All @@ -47,7 +49,7 @@ To test MCP tools interactively, use the MCP Inspector:
npx @modelcontextprotocol/inspector tsx mcp/src/index.ts
```

CI runs typecheck, unit tests, and E2E tests on every PR. Make sure they pass locally before pushing.
CI runs typecheck, lint (`oxlint`), unit tests, and E2E tests on every PR, and lint is also enforced pre-commit via `prek` alongside typecheck and unit tests. Make sure they pass locally before pushing.

To run a specific test file:

Expand Down Expand Up @@ -88,6 +90,8 @@ Branch from `main`. Use one of these prefixes:

- `feat/` — new functionality
- `fix/` — bug fixes
- `perf/` — performance changes
- `ci/` — CI/release workflow changes
- `docs/` — documentation only
- `refactor/` — code changes with no behavior change
- `test/` — test additions or fixes
Expand All @@ -99,7 +103,7 @@ Commits follow [Conventional Commits](https://www.conventionalcommits.org/):
type(scope): description
```

Scope is optional. Use present-indicative mood: "adds feature", not "add feature" or "added feature".
Scope is optional. Use imperative mood: "add feature", not "adds feature" or "added feature".

## Releasing the MCP server

Expand Down Expand Up @@ -145,6 +149,6 @@ git push upstream main --follow-tags

All PRs target `main` on `gordon-code/github-tracker`. Keep PRs focused — one feature or fix per PR makes review faster and reverts cleaner.

In the PR body, describe what changed and why. CI runs typecheck, unit tests, and E2E tests automatically. PRs need a passing CI run before merge.
In the PR body, describe what changed and why. CI runs typecheck, lint, unit tests, and E2E tests automatically. PRs need a passing CI run before merge.

When adding or changing user-facing features, update [docs/USER_GUIDE.md](docs/USER_GUIDE.md) to reflect the changes.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ pnpm run dev # Start Vite dev server
pnpm test # Run unit/component tests
pnpm test:e2e # Run Playwright E2E tests
pnpm run typecheck # TypeScript check
pnpm lint # Run type-aware linter (oxlint)
pnpm run build # Production build
pnpm run screenshot # Capture dashboard screenshot
```
Expand Down
2 changes: 1 addition & 1 deletion mcp/src/octokit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export async function validateTokenScopes(): Promise<boolean> {

try {
const response = await client.request("GET /user");
const login = String((response.data as { login?: string }).login ?? "unknown");
const login = (response.data as { login?: string }).login ?? "unknown";
const rawScopeHeader = (response.headers as Record<string, string | undefined>)["x-oauth-scopes"];

if (rawScopeHeader === undefined) {
Expand Down
2 changes: 1 addition & 1 deletion mcp/src/resources.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ── MCP resource registration ─────────────────────────────────────────────────
// Registers tracker:// resources with the MCP server.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { DataSource } from "./data-source.js";

// ── Resource registration ─────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion mcp/src/tools.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ── MCP tool registration ─────────────────────────────────────────────────────
// Registers all 6 GitHub Tracker tools with the MCP server.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { METHODS } from "../../src/shared/protocol.js";
import type { DataSource, CachedConfig } from "./data-source.js";
Expand Down
2 changes: 1 addition & 1 deletion mcp/src/ws-relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export function startWebSocketServer(): WebSocketServer | null {
info: { origin: string; req: IncomingMessage; secure: boolean },
callback: (res: boolean, code?: number, message?: string) => void
): void {
const origin = info.req.headers.origin as string | undefined;
const origin = info.req.headers.origin;
if (!isOriginAllowed(origin)) {
console.error(`[mcp/ws] Rejected connection from disallowed origin: ${origin ?? "(none)"}`);
callback(false, 403, "Origin not allowed");
Expand Down
Loading
Loading