feat: bounded retry for transient API failures#23
Merged
Conversation
…euse buffer on UTF-8 failure
## Description This pull request optimizes multi-file commits by using the GitHub `createTree` API's inline `content` field for text files so blobs are created server-side, instead of issuing one `createBlob` request per file. Binary files (NUL byte in the first 8 KiB) still use `createBlob` with base64 encoding. Large change sets are split into chained `createTree` calls (100 entries per request) to stay within payload limits. This reduces REST traffic for large commits (for example, on the order of five calls plus ref/commit steps for many text files, instead of one blob per file) and is easier on GitHub App secondary rate limits. Commits remain API-created and verified the same way as before; authentication is unchanged. ## Related Issue(s) Closes #19 ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [x] Documentation update - [ ] Refactoring (no functional changes) - [x] Test updates ## Changes Made - Added `isBinaryFile`, `getFileMode`, and exported `TREE_ENTRY_CHUNK_SIZE`; refactored `createTree` to use inline `content` for text, `createBlob` for binary, and chunked tree creation when there are more than 100 entries. - Binary blob creation is sequential (not concurrent) to avoid secondary rate-limit bursts. - Non-UTF-8 text files: `TextDecoder({ fatal: true })` with `createBlob` fallback to avoid silent data corruption. - `isBinaryFile`: uses `bytesRead` and scans only `buf.subarray(0, bytesRead)` to avoid false positives from zero-filled buffer tails. - Extended Jest setup and `commit.test.ts` for inline trees, binary/mixed paths, chunking, empty `filePaths` handling, and new helpers; clarified fs mock comment for binary detection. - Documented behavior under `[Unreleased]` in `CHANGELOG.md` (Added / Changed / Fixed). Updated `README.md` (features, module exports, example `uses` pin to `@main` with note that inline optimization is unreleased until next tag). - Regenerated `dist/` via `npm run build` and `npm run bundle`. ## Testing - [x] Tests pass locally (`npm test`) - [ ] Manual testing performed (describe below) ### Manual Testing Details Not run against the live GitHub API in this environment; behavior is covered by unit tests with mocked Octokit. ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have updated the documentation accordingly (README.md, CONTRIBUTE.md, etc.) - [x] I have updated the CHANGELOG.md in the `[Unreleased]` section - [x] My changes generate no new warnings or errors - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published ## Additional Notes - Branch was renamed from `bugfix/19-createtree-api-rate-limit` to `feature/19-createtree-api-rate-limit`; issue #19 was recategorized as an enhancement with an updated title. - Commits are intentionally atomic: tests (red), feature, docs, dist rebuild; plus Copilot follow-ups (empty createTree test, fs mock comment, README pin to `@main`, dist). - Payload-size-aware chunking (large inline content) is tracked in #22.
Prepare tests for isTransientError, classifyError, calculateDelay, and withRetry. TDD Red phase.
- Add isTransientError for 404, 5xx, 429, 403 rate-limit - Add classifyError for human-readable logging - Add calculateDelay with exponential backoff + jitter - Add withRetry wrapper (transient-only, bounded attempts) - TDD Green: all retry tests pass
TDD Red: tests for bounded retry on getRef, getCommit, createTree, createCommit, updateRef. commitViaAPI does not yet use withRetry.
- Add maxAttempts, logger, baseDelayMs, maxDelayMs to CommitOptions - Wrap getBranchInfo, getCommit, createTree, createCommit, updateBranch in withRetry - Default maxAttempts=1 (no retries) for backward compatibility - TDD Green: all commit retry tests pass
TDD Red: tests for parsing MAX_ATTEMPTS and clamping invalid values. Runner does not yet read MAX_ATTEMPTS.
- Read MAX_ATTEMPTS env var (default 1 = no retries) - Clamp invalid values to 1 with info log - Pass maxAttempts to commitViaAPI - TDD Green: all commit-runner MAX_ATTEMPTS tests pass
- Add MAX_ATTEMPTS to Action and CLI usage - Document transient error types and backoff behavior - Document CommitOptions.maxAttempts and logger
There was a problem hiding this comment.
Pull request overview
Adds bounded retry with exponential backoff (and jitter) around GitHub REST calls to reduce flakiness from transient API errors, and wires it into the action via a MAX_ATTEMPTS env var / CommitOptions configuration. The PR also updates the commit implementation to reduce REST call volume by inlining text file content into createTree, adds binary detection + tree batching, and refreshes docs/tests/dist artifacts.
Changes:
- Introduces
src/retry.ts(isTransientError,classifyError,calculateDelay,withRetry) and wraps key API operations incommitViaAPIwith retry. - Adds
MAX_ATTEMPTSparsing incommit-runner.tsand passes retry config/logging intocommitViaAPI. - Optimizes tree creation (inline content for UTF-8 text, binary detection, chained/batched
createTree) and expands unit tests + docs + compileddist.
Reviewed changes
Copilot reviewed 9 out of 32 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/retry.ts | New retry utilities (classification, backoff, bounded retry wrapper). |
| src/commit.ts | Wraps core GitHub API operations with retry; adds tree creation optimizations and helpers. |
| src/commit-runner.ts | Parses MAX_ATTEMPTS and passes retry config/logger to commitViaAPI. |
| src/tests/unit/retry.test.ts | Unit tests for transient classification, delay calculation, and retry behavior. |
| src/tests/unit/commit.test.ts | Expanded tests for new tree behavior, binary detection, and commit retries. |
| src/tests/unit/commit-runner.test.ts | Tests MAX_ATTEMPTS parsing/clamping behavior. |
| src/tests/setup.ts | Extends mocked fs with sync fd APIs for binary detection tests. |
| dist/src/retry.js.map | Built sourcemap for retry module. |
| dist/src/retry.js | Built JS for retry module. |
| dist/src/retry.d.ts.map | Built TS declaration sourcemap for retry. |
| dist/src/retry.d.ts | Built TS declarations for retry. |
| dist/src/commit.js.map | Updated built sourcemap for commit module changes. |
| dist/src/commit.js | Updated built JS for commit module changes (retry + tree optimization). |
| dist/src/commit.d.ts.map | Updated built TS declaration sourcemap for commit module. |
| dist/src/commit.d.ts | Updated built TS declarations for commit module public surface changes. |
| dist/src/commit-runner.js.map | Updated built sourcemap for runner changes. |
| dist/src/commit-runner.js | Updated built JS for runner changes (MAX_ATTEMPTS parsing). |
| dist/src/commit-runner.d.ts.map | Updated built TS declaration sourcemap for runner. |
| dist/src/commit-runner.d.ts | Updated built TS declarations for runner docs/header. |
| dist/src/tests/unit/retry.test.js.map | Built sourcemap for retry unit tests. |
| dist/src/tests/unit/retry.test.js | Built JS for retry unit tests. |
| dist/src/tests/unit/retry.test.d.ts.map | Built TS declaration sourcemap for retry unit tests. |
| dist/src/tests/unit/retry.test.d.ts | Built TS declarations for retry unit tests. |
| dist/src/tests/unit/commit.test.js | Updated built JS for commit unit tests. |
| dist/src/tests/unit/commit-runner.test.js.map | Updated built sourcemap for runner unit tests. |
| dist/src/tests/unit/commit-runner.test.js | Updated built JS for runner unit tests. |
| dist/src/tests/setup.js.map | Updated built sourcemap for test setup changes. |
| dist/src/tests/setup.js | Updated built JS for test setup changes. |
| dist/index.js | Updated bundled action output including retry + runner + commit changes. |
| README.md | Documents MAX_ATTEMPTS and new module exports; updates usage example. |
| CHANGELOG.md | Adds [Unreleased] entries describing new functionality/behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Require typeof status === 'number' in hasStatus type guard - Clamp maxAttempts to >= 1 (and non-finite to 1) in withRetry - Add tests for non-numeric status and zero/negative maxAttempts
Delegate base64 blob creation to createBlob for consistency with binary path.
Use @v0.1.5 in the primary workflow example; unreleased features remain documented in the note below.
c-vigo
added a commit
that referenced
this pull request
Mar 24, 2026
Adds missing `[Unreleased]` CHANGELOG entries for PR #23 (bounded retry for transient API failures) and the Copilot review follow-ups: - **Added:** Bounded retry (MAX_ATTEMPTS, CommitOptions, retry module) — issue #20 - **Changed:** README example pinned to @v0.1.5 - **Fixed:** withRetry normalization, hasStatus type guard, createBlob fallback consistency
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds bounded retry with exponential backoff for transient GitHub API failures. When the API returns transient errors (404, 5xx, 429, or 403 with rate-limit/abuse message), the action can automatically retry with configurable attempts instead of failing immediately.
Key additions:
src/retry.ts—isTransientError,classifyError,calculateDelay, andwithRetryutilitiesMAX_ATTEMPTSenv var — controls retry count (default 1 = no retries for backward compatibility)commitViaAPI(getBranchInfo,getCommit,createTree,createCommit,updateBranch) wrapped inwithRetryRelated Issue(s)
Closes #20
Type of Change
Changes Made
src/retry.tswithRetryConfig,isTransientError,classifyError,calculateDelay, andwithRetryCommitOptionswithmaxAttempts,logger,baseDelayMs,maxDelayMsgetBranchInfo,getCommit,createTree,createCommit,updateBranchinwithRetryMAX_ATTEMPTSenv parsing incommit-runner.ts(invalid values clamped to 1)MAX_ATTEMPTSandCommitOptionsretry optionsTesting
npm test)Manual Testing Details
Unit tests cover:
isTransientError(404, 5xx, 429, 403 rate-limit message),classifyError,calculateDelay(backoff, jitter, max cap),withRetry(success, exhaust, non-transient no-retry, logger), commit retry behavior (maxAttempts 1/2/3), and MAX_ATTEMPTS env parsing.Checklist
[Unreleased]sectionAdditional Notes
maxAttempts: 1preserves existing behavior (no retries) so this is backward compatibleRequestErrorexposes this in the message string)