Skip to content

[1.4] Set Up GitHub API Client #92

Description

@Sorcecoder

Story 1.4: Set Up GitHub API Client

Status: ready-for-dev

Story

As a developer,
I want a configured GitHub API client using GraphQL,
So that I can efficiently fetch stargazer data.

Acceptance Criteria

  1. Given GitHub API credentials are configured via environment variables
    When I initialize the GitHub client
    Then the client is configured with GraphQL endpoint connection

  2. And authentication token management is handled securely (token from env vars, never logged)

  3. And request timeout configuration is set to 30 seconds

  4. And retry logic with exponential backoff is implemented (max 3 retries)

  5. Given a properly configured client
    When I execute a test query
    Then the client can successfully fetch basic repository data (e.g., stargazer count)

  6. And rate limit information is accessible from responses (remaining, reset_at)

Tasks / Subtasks

  • Task 1: Install GitHub GraphQL client library (AC: Test issue #1)

    • Install @octokit/graphql package
    • Add TypeScript types (@octokit/graphql includes types)
    • Verify installation with npm ls @octokit/graphql
  • Task 2: Create GitHub client module (AC: Test issue #1, [1.1] Project Initialization and Flask API Setup #2, [1.2] Health Check Endpoint #3)

    • Create src/services/github.ts with client factory
    • Configure authentication using GITHUB_TOKEN environment variable
    • Set request timeout to 30 seconds
    • Use singleton pattern for connection reuse (Cloudflare Workers warm instances)
  • Task 3: Implement retry logic with exponential backoff (AC: [1.3] Request Logging Infrastructure #4)

    • Create retry wrapper function with max 3 attempts
    • Implement exponential backoff: 1s, 2s, 4s delays
    • Only retry on transient errors (5xx, network errors), not 4xx client errors
    • Log retry attempts with structured logging (no sensitive data)
  • Task 4: Implement rate limit extraction (AC: [2.1] API Key Data Model and Storage #6)

    • Parse x-ratelimit-remaining and x-ratelimit-reset from response headers
    • Create RateLimitInfo type interface
    • Expose rate limit data in client response wrapper
  • Task 5: Create test query function (AC: [1.4] Metrics Collection and Success/Failure Tracking #5)

    • Implement testGitHubConnection() function
    • Query a simple repository field (e.g., repository { stargazerCount })
    • Return success/failure status with error details if applicable
  • Task 6: Integrate with health endpoint (AC: [1.4] Metrics Collection and Success/Failure Tracking #5)

    • Add github_api check to health endpoint from Story 1.1
    • Return { status: 'ok', rate_limit: { remaining, reset_at } } on success
    • Return { status: 'error', error: string } on failure
  • Task 7: Update environment configuration (AC: [1.1] Project Initialization and Flask API Setup #2)

    • Add GITHUB_TOKEN to .env.example with documentation
    • Document token scope requirements (read:user, repo for stargazers)

Dev Notes

Technical Stack Requirements

  • Runtime: Cloudflare Workers (established in Story 1.1)
  • Language: TypeScript with strict mode
  • GitHub Client: @octokit/graphql - official GitHub GraphQL client with TypeScript support

Required Dependencies

{
  "dependencies": {
    "@octokit/graphql": "^8.0.0"
  }
}

Client Implementation Pattern

// src/services/github.ts
import { graphql } from '@octokit/graphql';

interface GitHubClientConfig {
  token: string;
  timeout?: number;
}

interface RateLimitInfo {
  remaining: number;
  resetAt: string; // ISO 8601 timestamp
}

interface GitHubResponse<T> {
  data: T;
  rateLimit: RateLimitInfo;
}

let graphqlClient: typeof graphql | null = null;

export function getGitHubClient(): typeof graphql {
  if (!graphqlClient) {
    const token = process.env.GITHUB_TOKEN;
    if (!token) {
      throw new Error('GITHUB_TOKEN environment variable is required');
    }

    graphqlClient = graphql.defaults({
      headers: {
        authorization: `bearer ${token}`,
      },
      request: {
        timeout: 30000, // 30 seconds
      },
    });
  }
  return graphqlClient;
}

Retry Logic Pattern

// src/utils/retry.ts
interface RetryOptions {
  maxAttempts: number;
  initialDelayMs: number;
  maxDelayMs: number;
}

const DEFAULT_RETRY_OPTIONS: RetryOptions = {
  maxAttempts: 3,
  initialDelayMs: 1000,
  maxDelayMs: 4000,
};

export async function withRetry<T>(
  fn: () => Promise<T>,
  options: RetryOptions = DEFAULT_RETRY_OPTIONS
): Promise<T> {
  let lastError: Error | null = null;

  for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;

      // Don't retry on client errors (4xx)
      if (isClientError(error)) {
        throw error;
      }

      if (attempt < options.maxAttempts) {
        const delay = Math.min(
          options.initialDelayMs * Math.pow(2, attempt - 1),
          options.maxDelayMs
        );
        await sleep(delay);
      }
    }
  }

  throw lastError;
}

GraphQL Query Examples

Test Connection Query:

query TestConnection($owner: String!, $repo: String!) {
  repository(owner: $owner, name: $repo) {
    stargazerCount
    name
  }
  rateLimit {
    remaining
    resetAt
  }
}

Stargazers Query (for reference - will be fully implemented in Epic 2):

query GetStargazers($owner: String!, $repo: String!, $first: Int!, $after: String) {
  repository(owner: $owner, name: $repo) {
    stargazers(first: $first, after: $after) {
      totalCount
      pageInfo {
        hasNextPage
        endCursor
      }
      edges {
        starredAt
        node {
          login
          name
          email
          company
          location
          bio
          websiteUrl
          twitterUsername
          avatarUrl
        }
      }
    }
  }
  rateLimit {
    remaining
    resetAt
  }
}

Rate Limit Information

GitHub GraphQL API rate limits:

  • Authenticated requests: 5,000 points/hour (queries cost variable points based on complexity)
  • Rate limit headers: Available in response metadata
  • GraphQL rateLimit field: Include in queries to get remaining/resetAt

Important: Always include rateLimit field in queries to track consumption.

Environment Variables

Variable Required Description
GITHUB_TOKEN Yes GitHub Personal Access Token or GitHub App token

Token Scope Requirements:

  • read:user - For accessing user profile data
  • public_repo or repo - For accessing repository stargazers

Security Requirements (NFR-3.3)

  • GitHub token stored server-side only, never exposed to clients
  • Never log the token or include in error messages
  • Use environment variables only (no hardcoded tokens)
  • Sanitize error messages before returning to clients

File Structure

src/
├── services/
│   ├── github.ts         # GitHub GraphQL client factory
│   └── redis.ts          # From Story 1.3
├── utils/
│   ├── retry.ts          # Retry logic with exponential backoff
│   └── env.ts            # Environment variable helpers
├── types/
│   ├── github.ts         # GitHub API types (RateLimitInfo, etc.)
│   └── health.ts         # Health check types
└── handlers/
    └── health.ts         # Health endpoint (update to include GitHub check)

Integration with Previous Stories

From Story 1.1 (Project Structure):

  • Health endpoint at /health already exists - extend it
  • TypeScript strict mode is configured
  • Cloudflare Workers runtime established

From Story 1.3 (Redis):

  • Redis health check pattern to follow:
export async function checkGitHubHealth(): Promise<HealthStatus> {
  const start = Date.now();
  try {
    const result = await testGitHubConnection();
    return {
      status: 'ok',
      latencyMs: Date.now() - start,
      rateLimit: result.rateLimit
    };
  } catch (error) {
    return {
      status: 'error',
      error: error.message,
      latencyMs: Date.now() - start
    };
  }
}

Error Handling

Use @octokit/graphql's GraphqlResponseError for distinguishing GraphQL errors:

import { GraphqlResponseError } from '@octokit/graphql';

try {
  const result = await graphql(query, variables);
} catch (error) {
  if (error instanceof GraphqlResponseError) {
    // GraphQL-specific error (e.g., validation, not found)
    console.error('GraphQL Error:', error.errors);
    // Partial data may be available at error.data
  } else {
    // Network or other error
    throw error;
  }
}

Anti-Patterns to Avoid

  1. DO NOT use template literals in GraphQL queries - vulnerable to injection attacks
  2. DO NOT create new client instances per request - use singleton pattern
  3. DO NOT log or expose the GitHub token in any way
  4. DO NOT retry on 4xx client errors (invalid token, not found, etc.)
  5. DO NOT ignore rate limit information - always track remaining calls
  6. DO NOT block application startup on GitHub connectivity - use lazy initialization

Testing Requirements

  • Unit test client factory with mocked @octokit/graphql
  • Unit test retry logic with various failure scenarios
  • Integration test with GitHub API (use a test repository)
  • Test rate limit extraction from actual responses
  • Test error handling for invalid token, network errors, GraphQL errors

Cloudflare Workers Considerations

  • No file system access - all configuration via environment variables
  • Use Cloudflare Workers bindings for secrets in production (wrangler secret put GITHUB_TOKEN)
  • Connection pooling handled differently in Workers vs Node.js
  • Workers have 30 second CPU time limit per request - timeout must be less

References

  • [Source: _bmad-output/planning-artifacts/epics.md#Story-1.4] - Original story requirements
  • [Source: _bmad-output/planning-artifacts/prd.md#Technical-Architecture-Considerations] - GraphQL API for efficient data fetching
  • [Source: _bmad-output/planning-artifacts/prd.md#GitHub-API-Integration] - Batch stargazer queries, pagination
  • [Source: _bmad-output/planning-artifacts/prd.md#NFR-3.3] - GitHub token security requirements
  • GitHub GraphQL API Documentation
  • @octokit/graphql GitHub Repository

Dev Agent Record

Agent Model Used

{{agent_model_name_version}}

Debug Log References

Completion Notes List

File List

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions