Contributing

Guidelines for contributing to ShipKit, including code standards, pull request process, testing requirements, and documentation

Contributing to ShipKit

Thank you for your interest in contributing to ShipKit! This guide will help you understand our development process and how to contribute effectively.

Code of Conduct

We are committed to providing a welcoming and inclusive experience for everyone. Please read our Code of Conduct before contributing.

Getting Started

  1. Fork the Repository

    # Clone your fork
    git clone https://github.com/your-username/shipkit.git
    cd shipkit
    
    # Add upstream remote
    git remote add upstream https://github.com/your-org/shipkit.git
    
  2. Install Dependencies

    # Install Node.js 20 (LTS)
    nvm use 20
    
    # Install PNPM
    npm install -g pnpm@latest
    
    # Install project dependencies
    pnpm install
    
  3. Set Up Development Environment

    # Copy environment variables
    cp .env.example .env
    
    # Generate Prisma client
    pnpm prisma generate
    
    # Run database migrations
    pnpm prisma migrate dev
    

Development Process

Branch Naming

  • Feature: feature/description
  • Bug Fix: fix/description
  • Documentation: docs/description
  • Performance: perf/description
  • Refactor: refactor/description

Commit Messages

We use Conventional Commits for clear communication:

# Format
type(scope): description

# Examples
feat(auth): add social login providers
fix(api): handle undefined user data
docs(readme): update deployment instructions
perf(queries): optimize database indexes

Code Style

// 1. Use TypeScript for type safety
interface User {
  id: string
  email: string
  name?: string
  role: 'USER' | 'ADMIN'
}

// 2. Use arrow functions for components
export const UserProfile = ({ user }: { user: User }) => {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}

// 3. Use function declarations for utilities
export function formatUserName(user: User): string {
  return user.name ?? user.email.split('@')[0]
}

// 4. Use constants for configuration
export const AUTH_CONFIG = {
  PROVIDERS: ['github', 'google'] as const,
  SESSION_MAX_AGE: 30 * 24 * 60 * 60, // 30 days
  COOKIE_NAME: 'shipkit-session',
} as const

// 5. Use error classes for custom errors
export class AuthenticationError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'AuthenticationError'
  }
}

Testing Requirements

  1. Unit Tests

    // src/__tests__/utils/format.test.ts
    import { formatUserName } from '@/utils/format'
    
    describe('formatUserName', () => {
      it('returns name when available', () => {
        const user = { id: '1', email: 'test@example.com', name: 'Test User', role: 'USER' }
        expect(formatUserName(user)).toBe('Test User')
      })
    
      it('returns email username when name is not available', () => {
        const user = { id: '1', email: 'test@example.com', role: 'USER' }
        expect(formatUserName(user)).toBe('test')
      })
    })
    
  2. Integration Tests

    // src/__tests__/api/auth.test.ts
    import { createMocks } from 'node-mocks-http'
    import { POST } from '@/app/api/auth/register/route'
    
    describe('POST /api/auth/register', () => {
      it('creates a new user', async () => {
        const { req, res } = createMocks({
          method: 'POST',
          body: {
            email: 'test@example.com',
            password: 'Password123!',
          },
        })
    
        await POST(req, res)
    
        expect(res._getStatusCode()).toBe(201)
        expect(JSON.parse(res._getData())).toHaveProperty('id')
      })
    })
    
  3. E2E Tests

    // e2e/auth.spec.ts
    import { test, expect } from '@playwright/test'
    
    test('user can sign in', async ({ page }) => {
      await page.goto('/auth/signin')
      await page.fill('input[name="email"]', 'test@example.com')
      await page.fill('input[name="password"]', 'Password123!')
      await page.click('button[type="submit"]')
    
      await expect(page).toHaveURL('/dashboard')
      await expect(page.locator('h1')).toContainText('Dashboard')
    })
    

Pull Request Process

  1. Create Feature Branch

    # Update main branch
    git checkout main
    git pull upstream main
    
    # Create feature branch
    git checkout -b feature/your-feature
    
  2. Make Changes

    # Stage changes
    git add .
    
    # Commit with conventional commit message
    git commit -m "feat: add new feature"
    
    # Push to your fork
    git push origin feature/your-feature
    
  3. Open Pull Request

    • Use the PR template
    • Link related issues
    • Add meaningful description
    • Include screenshots/videos if UI changes
    • Request reviews from maintainers
  4. Address Reviews

    # Make requested changes
    git add .
    git commit -m "fix: address review comments"
    git push origin feature/your-feature
    

Documentation

Code Comments

/**
 * Authenticates a user with email and password
 * @param email - User's email address
 * @param password - User's password (min 8 chars, 1 uppercase, 1 number, 1 special)
 * @returns User object if authentication successful
 * @throws AuthenticationError if credentials invalid
 *
 * @example
 * ```ts
 * const user = await authenticateUser('test@example.com', 'Password123!')
 * console.log(user.id) // '123'
 * ```
 */
export async function authenticateUser(
  email: string,
  password: string
): Promise<User> {
  // Validate input
  if (!email || !password) {
    throw new AuthenticationError('Email and password required')
  }

  // Check user exists
  const user = await db.user.findUnique({ where: { email } })
  if (!user) {
    throw new AuthenticationError('Invalid credentials')
  }

  // Verify password
  const isValid = await verifyPassword(password, user.hashedPassword)
  if (!isValid) {
    throw new AuthenticationError('Invalid credentials')
  }

  return user
}

API Documentation

/**
 * @api {post} /api/auth/register Register User
 * @apiName RegisterUser
 * @apiGroup Authentication
 * @apiVersion 1.0.0
 *
 * @apiParam {String} email User's email address
 * @apiParam {String} password User's password
 * @apiParam {String} [name] User's display name
 *
 * @apiSuccess {String} id User's unique ID
 * @apiSuccess {String} email User's email address
 * @apiSuccess {String} name User's display name
 * @apiSuccess {String} role User's role
 *
 * @apiError {Object} error Error object
 * @apiError {String} error.message Error message
 * @apiError {String} error.code Error code
 */
export async function POST(req: Request) {
  try {
    const body = await req.json()
    const user = await registerUser(body)
    return NextResponse.json(user, { status: 201 })
  } catch (error) {
    return handleError(error)
  }
}

Component Documentation

import { type ComponentProps } from 'react'
import { cn } from '@/lib/utils'

interface ButtonProps extends ComponentProps<'button'> {
  /** Visual variant of the button */
  variant?: 'default' | 'outline' | 'ghost'
  /** Size of the button */
  size?: 'sm' | 'md' | 'lg'
  /** Whether the button is in a loading state */
  loading?: boolean
}

/**
 * Primary button component for user interaction
 *
 * @example
 * ```tsx
 * <Button
 *   variant="outline"
 *   size="lg"
 *   onClick={() => console.log('clicked')}
 * >
 *   Click me
 * </Button>
 * ```
 */
export const Button = ({
  variant = 'default',
  size = 'md',
  loading = false,
  className,
  children,
  ...props
}: ButtonProps) => {
  return (
    <button
      className={cn(
        'button',
        `button--${variant}`,
        `button--${size}`,
        loading && 'button--loading',
        className
      )}
      disabled={loading}
      {...props}
    >
      {loading ? <Spinner /> : children}
    </button>
  )
}

Review Process

Code Review Checklist

  1. Functionality

    • [ ] Code works as expected
    • [ ] Edge cases handled
    • [ ] Error states managed
    • [ ] Performance considered
  2. Code Quality

    • [ ] TypeScript types used correctly
    • [ ] Code is readable and maintainable
    • [ ] No unnecessary complexity
    • [ ] Follows project conventions
  3. Testing

    • [ ] Unit tests added/updated
    • [ ] Integration tests added/updated
    • [ ] E2E tests added/updated
    • [ ] Test coverage maintained
  4. Documentation

    • [ ] Code comments added
    • [ ] API documentation updated
    • [ ] Component documentation updated
    • [ ] README updated if needed

Review Comments

// Good comment
// Consider using optional chaining here to handle undefined values
const userName = user?.name ?? 'Anonymous'

// Better comment with example
// Consider using optional chaining to handle undefined values
// Example: user = undefined -> 'Anonymous'
//         user = { name: null } -> 'Anonymous'
//         user = { name: 'John' } -> 'John'
const userName = user?.name ?? 'Anonymous'

Release Process

  1. Version Bump

    # Update version
    pnpm version patch # or minor/major
    
    # Generate changelog
    pnpm changelog
    
  2. Create Release

    # Create git tag
    git tag -a v1.0.0 -m "Release v1.0.0"
    
    # Push tag
    git push origin v1.0.0
    
  3. Deploy Release

    # Deploy to staging
    pnpm deploy:stage
    
    # Run tests
    pnpm test:e2e
    
    # Deploy to production
    pnpm deploy
    

Getting Help

Additional Resources