Skip to content

P1: GitHub API hardening (token support, retries, rate-limit messaging)#37

Merged
Kinin-Code-Offical merged 1 commit intomainfrom
p1/github-api-hardening
Dec 22, 2025
Merged

P1: GitHub API hardening (token support, retries, rate-limit messaging)#37
Kinin-Code-Offical merged 1 commit intomainfrom
p1/github-api-hardening

Conversation

@Kinin-Code-Offical
Copy link
Owner

@Kinin-Code-Offical Kinin-Code-Offical commented Dec 22, 2025

Closes #20

Summary:

  • Add GitHub API request helper with retries and optional token header.
  • Surface rate-limit reset time when remaining quota is zero.
  • Reuse helper for release lookups and checksum fetch.

Tests:

  • npm ci (PASS)
  • npm run lint (PASS)
  • npm test (PASS)
  • npm run build (PASS)

Rollback:

  • Revert this commit to restore direct API calls without retries/token support.

Summary by Sourcery

Harden GitHub API interactions used for self-update by introducing a shared request helper with retries, token-based authentication, and improved rate-limit handling.

Enhancements:

  • Introduce a reusable GitHub GET helper that adds optional token authentication, handles retryable errors, and logs rate-limit reset information.
  • Update release and prerelease lookup logic to use the shared GitHub helper instead of direct axios calls.
  • Ensure checksum downloads also send consistent GitHub headers, including optional authentication tokens.

Copilot AI review requested due to automatic review settings December 22, 2025 00:24
@sourcery-ai
Copy link

sourcery-ai bot commented Dec 22, 2025

Reviewer's Guide

Adds a reusable GitHub API helper that injects optional token-based auth, centralizes retry/backoff logic with rate-limit-aware logging, and refactors existing release and checksum fetchers to use it.

Class-style diagram for selfUpdate GitHub helper and API functions

classDiagram
    class SelfUpdateModule {
        <<module>>
        +getLatestRelease() Promise~ReleaseInfo~
        +getReleaseByTag(tag string) Promise~ReleaseInfo~
        +getLatestPrerelease() Promise~ReleaseInfo~
        +fetchSha256Sums(release ReleaseInfo) Promise~Map~string,string~
    }

    class GitHubHelpers {
        <<utility>>
        +getGithubHeaders() Record~string,string~
        +getRateLimitMessage(error any) string
        +shouldRetry(error any) boolean
        +githubGet~T~(url string) Promise~AxiosResponse~T~~
    }

    class ReleaseInfo {
        +tag_name string
        +name string
        +body string
        +prerelease boolean
        +draft boolean
        +assets any[]
    }

    class UpdateStatus {
        +updateAvailable boolean
        +currentVersion string
        +latestVersion string
        +releaseInfo ReleaseInfo
    }

    SelfUpdateModule ..> GitHubHelpers : uses
    SelfUpdateModule ..> ReleaseInfo : returns
    SelfUpdateModule ..> UpdateStatus : returns
    GitHubHelpers ..> ReleaseInfo : typed responses
Loading

Flow diagram for githubGet retry and backoff logic

flowchart TD
    A["start githubGet(url)"] --> B["set attempt = 0"]
    B --> C["attempt <= MAX_RETRIES?"]
    C -->|no| Z["throw Error GitHub API request failed after retries"]
    C -->|yes| D["try axios.get(url, timeout, headers from getGithubHeaders)"]
    D --> E{"request succeeded?"}
    E -->|yes| F["return response"]
    E -->|no| G["attempt++"]
    G --> H["getRateLimitMessage(error)"]
    H --> I{"rate limit message?"}
    I -->|yes| J["logger.warn(rate limit reset time)"]
    I -->|no| K["skip logging"]
    J --> L["shouldRetry(error)?"]
    K --> L["shouldRetry(error)?"]
    L -->|no or attempt > MAX_RETRIES| M["throw error"]
    L -->|yes and attempt <= MAX_RETRIES| N["delay 1000ms * attempt"]
    N --> C
Loading

File-Level Changes

Change Details Files
Introduce a shared GitHub GET helper with retries, backoff, token support, and rate-limit messaging, and refactor release lookups to use it.
  • Add RETRYABLE_STATUS set and shouldRetry helper to decide which HTTP errors trigger retries, including network errors and common 5xx/429 statuses
  • Add getGithubHeaders to always send User-Agent and optionally attach a Bearer token from CLOUDSQLCTL_GITHUB_TOKEN or GITHUB_TOKEN
  • Add getRateLimitMessage to derive a human-readable reset time from GitHub rate-limit headers and log it via logger.warn
  • Implement githubGet wrapper around axios.get with timeout, headers, exponential-ish backoff per attempt, and a final failure error
  • Update getLatestRelease, getReleaseByTag, and getLatestPrerelease to call githubGet with typed responses instead of inlining axios config
src/core/selfUpdate.ts
Ensure checksum downloads also use the hardened GitHub headers, including token when available.
  • Extend existing axios.get call for checksum asset download to include getGithubHeaders while preserving timeout and responseType:text behavior
src/core/selfUpdate.ts

Assessment against linked issues

Issue Objective Addressed Explanation
#20 Add support for authenticating GitHub API requests using an optional token.
#20 Introduce a reusable GitHub API request helper that adds retries for transient failures.
#20 Surface clearer messaging when GitHub API rate limits are hit, including reset timing.

Possibly linked issues

  • #P1: PR adds token headers, retry logic, and rate-limit messaging, exactly fulfilling the GitHub API hardening issue

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • For consistency and to benefit from the retry logic, consider routing the checksum fetch in fetchSha256Sums through githubGet instead of calling axios.get directly.
  • In getRateLimitMessage, it may be safer to validate that x-ratelimit-reset is a parseable numeric timestamp before using Number(reset) to avoid logging an invalid date if the header is malformed or missing.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- For consistency and to benefit from the retry logic, consider routing the checksum fetch in `fetchSha256Sums` through `githubGet` instead of calling `axios.get` directly.
- In `getRateLimitMessage`, it may be safer to validate that `x-ratelimit-reset` is a parseable numeric timestamp before using `Number(reset)` to avoid logging an invalid date if the header is malformed or missing.

## Individual Comments

### Comment 1
<location> `src/core/selfUpdate.ts:46-51` </location>
<code_context>
+    if (axios.isAxiosError(error) && error.response) {
+        const remaining = error.response.headers['x-ratelimit-remaining'];
+        const reset = error.response.headers['x-ratelimit-reset'];
+        if (remaining === '0' && reset) {
+            const resetTime = new Date(Number(reset) * 1000).toISOString();
+            return `GitHub API rate limit exceeded. Resets at ${resetTime}.`;
</code_context>

<issue_to_address>
**suggestion:** Handle non-numeric or unexpected `x-ratelimit-reset` values more defensively.

If `x-ratelimit-reset` is present but not a valid epoch, `Number(reset)` becomes `NaN` and `new Date(NaN).toISOString()` yields `Invalid Date`, producing a misleading message. You could guard with:

```ts
const resetEpoch = Number(reset);
if (!Number.isFinite(resetEpoch)) return null;
const resetTime = new Date(resetEpoch * 1000).toISOString();
```

This avoids logging invalid timestamps.

```suggestion
        const remaining = error.response.headers['x-ratelimit-remaining'];
        const reset = error.response.headers['x-ratelimit-reset'];
        if (remaining === '0' && reset) {
            const resetEpoch = Number(reset);
            if (!Number.isFinite(resetEpoch)) return null;

            const resetTime = new Date(resetEpoch * 1000).toISOString();
            return `GitHub API rate limit exceeded. Resets at ${resetTime}.`;
        }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +46 to +51
const remaining = error.response.headers['x-ratelimit-remaining'];
const reset = error.response.headers['x-ratelimit-reset'];
if (remaining === '0' && reset) {
const resetTime = new Date(Number(reset) * 1000).toISOString();
return `GitHub API rate limit exceeded. Resets at ${resetTime}.`;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Handle non-numeric or unexpected x-ratelimit-reset values more defensively.

If x-ratelimit-reset is present but not a valid epoch, Number(reset) becomes NaN and new Date(NaN).toISOString() yields Invalid Date, producing a misleading message. You could guard with:

const resetEpoch = Number(reset);
if (!Number.isFinite(resetEpoch)) return null;
const resetTime = new Date(resetEpoch * 1000).toISOString();

This avoids logging invalid timestamps.

Suggested change
const remaining = error.response.headers['x-ratelimit-remaining'];
const reset = error.response.headers['x-ratelimit-reset'];
if (remaining === '0' && reset) {
const resetTime = new Date(Number(reset) * 1000).toISOString();
return `GitHub API rate limit exceeded. Resets at ${resetTime}.`;
}
const remaining = error.response.headers['x-ratelimit-remaining'];
const reset = error.response.headers['x-ratelimit-reset'];
if (remaining === '0' && reset) {
const resetEpoch = Number(reset);
if (!Number.isFinite(resetEpoch)) return null;
const resetTime = new Date(resetEpoch * 1000).toISOString();
return `GitHub API rate limit exceeded. Resets at ${resetTime}.`;
}

@Kinin-Code-Offical Kinin-Code-Offical merged commit 72bc82e into main Dec 22, 2025
9 checks passed
@Kinin-Code-Offical Kinin-Code-Offical deleted the p1/github-api-hardening branch December 22, 2025 00:26
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the GitHub API integration by adding authentication token support, implementing retry logic for transient failures, and providing better rate-limit messaging. The changes centralize GitHub API requests through a new githubGet helper function.

Key Changes:

  • Introduced githubGet helper with retry logic (up to 2 retries) for handling transient HTTP errors
  • Added support for GitHub authentication tokens via CLOUDSQLCTL_GITHUB_TOKEN or GITHUB_TOKEN environment variables
  • Implemented rate-limit detection that surfaces the reset timestamp when quota is exhausted

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +74 to +78
const rateLimit = getRateLimitMessage(error);
if (rateLimit) {
logger.warn(rateLimit);
}
if (attempt > MAX_RETRIES || !shouldRetry(error)) {
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rate limit warning is logged on every retry attempt when rate limited, which could result in duplicate warnings. Consider only logging the rate limit message once, or restructuring the logic so the warning is shown before the final throw rather than on each retry attempt.

Suggested change
const rateLimit = getRateLimitMessage(error);
if (rateLimit) {
logger.warn(rateLimit);
}
if (attempt > MAX_RETRIES || !shouldRetry(error)) {
if (attempt > MAX_RETRIES || !shouldRetry(error)) {
const rateLimit = getRateLimitMessage(error);
if (rateLimit) {
logger.warn(rateLimit);
}

Copilot uses AI. Check for mistakes.
function shouldRetry(error: unknown): boolean {
if (axios.isAxiosError(error)) {
if (!error.response) return true;
return RETRYABLE_STATUS.has(error.response.status);
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When GitHub API returns a 429 (rate limit exceeded), the code will retry with delays of 1 second and 2 seconds. However, rate limit resets typically occur at fixed times (shown in the reset header), not after a few seconds. Retrying immediately with short delays is unlikely to succeed and will waste time. Consider checking the reset time and either failing immediately with the reset message, or implementing a longer delay based on the actual reset time.

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +86
async function githubGet<T>(url: string) {
let attempt = 0;
while (attempt <= MAX_RETRIES) {
try {
return await axios.get<T>(url, {
timeout: TIMEOUT_MS,
headers: getGithubHeaders()
});
} catch (error) {
attempt++;
const rateLimit = getRateLimitMessage(error);
if (rateLimit) {
logger.warn(rateLimit);
}
if (attempt > MAX_RETRIES || !shouldRetry(error)) {
throw error;
}
const delayMs = 1000 * attempt;
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
throw new Error('GitHub API request failed after retries');
}
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new githubGet helper function introduces significant new behavior including retry logic, token authentication, and rate limit handling, but no tests have been added to verify this functionality. Consider adding tests to cover scenarios such as: successful retry after transient failure, rate limit detection and messaging, token header inclusion when environment variable is set, and failure after max retries.

Copilot uses AI. Check for mistakes.
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
throw new Error('GitHub API request failed after retries');
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is unreachable because the while loop condition ensures that when attempt > MAX_RETRIES, the error is thrown at line 79 within the catch block. The function will never reach this point. Consider removing this line or restructuring the retry logic.

Suggested change
throw new Error('GitHub API request failed after retries');

Copilot uses AI. Check for mistakes.
Comment on lines 169 to 173
const response = await axios.get(checksumAsset.browser_download_url, {
responseType: 'text',
timeout: TIMEOUT_MS
timeout: TIMEOUT_MS,
headers: getGithubHeaders()
});
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetchSha256Sums function uses axios.get directly instead of using the new githubGet helper. This means it lacks retry logic and won't benefit from the token authentication when downloading checksums. Consider using githubGet here as well, or if the responseType: 'text' option is needed, consider extending githubGet to accept additional axios options.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P1: GitHub API hardening (token support, retries, rate-limit messaging)

1 participant