Repository navigation
Add retry with backoff for component git fetch - #208
Merged
Merged
Conversation
Component fetch shells out to a single Git.clone call with no retry logic, so any transient git/network failure while cloning an hl-component-* repo bubbles up as an unrescued exception in the caller and crashes the whole cfhighlander run. Wrap the clone call with a small retry loop using exponential backoff, configurable via CFHIGHLANDER_COMPONENT_FETCH_RETRIES (default 3) and CFHIGHLANDER_COMPONENT_FETCH_RETRY_DELAY (default 1 second), following the existing CFHIGHLANDER_* env var convention. Each retry is logged to STDERR. Once retries are exhausted the existing failure handling is unchanged: log and return nil. Add a focused spec covering both the retry-then-succeed path and the give-up-after-exhausting-retries path, stubbing sleep so the spec runs instantly.
There was a problem hiding this comment.
🟡 Changes recommended
Exception handling, configuration validation, and test environment isolation need correction.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds configurable retry with exponential backoff for failed component Git clones.
Changes:
- Retries
Git.clonewith configurable limits and delays. - Documents the new environment variables.
- Adds success and exhaustion tests.
File summaries
| File | Description |
|---|---|
lib/cfhighlander.factory.templatefinder.rb |
Implements clone retries and backoff. |
README.md |
Documents retry configuration. |
spec/test_templatefinder_spec.rb |
Tests successful retries and exhaustion. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+77
to
+78
| max_retries = (ENV['CFHIGHLANDER_COMPONENT_FETCH_RETRIES'] || 3).to_i | ||
| retry_delay = (ENV['CFHIGHLANDER_COMPONENT_FETCH_RETRY_DELAY'] || 1).to_i |
| attempt = 0 | ||
| begin | ||
| Git.clone git_url, cache_path, clone_opts | ||
| rescue Exception => clone_error |
Comment on lines
+11
to
+23
| around(:each) do |example| | ||
| Dir.mktmpdir do |dir| | ||
| @cache_path = File.join(dir, 'component') | ||
| example.run | ||
| end | ||
| end | ||
|
|
||
| before(:each) do | ||
| # keep the spec instant regardless of retry/delay env overrides | ||
| allow(finder).to receive(:sleep) | ||
| ENV.delete('CFHIGHLANDER_COMPONENT_FETCH_RETRIES') | ||
| ENV.delete('CFHIGHLANDER_COMPONENT_FETCH_RETRY_DELAY') | ||
| end |
- Narrow the retry loop's rescue clause to StandardError instead of Exception, so process-control signals like Interrupt/SystemExit during a clone are no longer swallowed and retried. - Validate CFHIGHLANDER_COMPONENT_FETCH_RETRIES and CFHIGHLANDER_COMPONENT_FETCH_RETRY_DELAY as non-negative integers, falling back to their documented defaults (3 and 1) on invalid, negative or non-numeric input instead of letting a bad value reach sleep and raise ArgumentError. - Restore the original env var values after each spec example via an around hook, and add coverage for the new fallback behavior.
Comment on lines
+45
to
+67
| it "retries a failing clone and succeeds once it stops failing" do | ||
| ENV['CFHIGHLANDER_COMPONENT_FETCH_RETRIES'] = '3' | ||
|
|
||
| attempts = 0 | ||
| allow(Git).to receive(:clone) do |_url, path, _opts| | ||
| attempts += 1 | ||
| raise Git::GitExecuteError, "simulated github flakiness" if attempts < 3 | ||
|
|
||
| FileUtils.mkdir_p path | ||
| File.write(File.join(path, 'mycomponent.cfhighlander.rb'), '') | ||
| end | ||
|
|
||
| component_name, location = finder.findTemplateGit( | ||
| @cache_path, 'mycomponent', 'latest', 'https://github.com/theonestack/hl-component-mycomponent', 'master' | ||
| ) | ||
|
|
||
| expect(attempts).to eq(3) | ||
| expect(component_name).to eq('mycomponent') | ||
| expect(location).to eq("#{@cache_path}/") | ||
| expect(finder).to have_received(:sleep).twice | ||
| expect(finder).to have_received(:sleep).with(1) | ||
| expect(finder).to have_received(:sleep).with(2) | ||
| end |
The existing tests only covered the default retry delay and the invalid-value fallback, so a custom CFHIGHLANDER_COMPONENT_FETCH_RETRY_DELAY override was never actually exercised. Add a test that sets the delay to a non-default value and asserts the exponential backoff sequence (delay, delay*2) it produces.
jaredbrook
approved these changes
Sep 7, 2026
Merged
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.
What
TemplateFinder#findTemplateGit(lib/cfhighlander.factory.templatefinder.rb) resolveshl-component-*dependencies with a single shallowGit.clonecall. Any transient failure from that call (network blip, flaky remote, etc.) is caught by the method's outerrescue Exception, logged, and turned into anilreturn — which the caller (ComponentFactory#loadComponentFromTemplate) turns into an unrescuedStandardError, crashing the whole CLI run for what is often a one-off, retryable failure.This adds a small retry-with-exponential-backoff loop around just the
Git.clonecall:CFHIGHLANDER_*convention (seeCFHIGHLANDER_WORKDIR,CFHIGHLANDER_AWS_RETRY_LIMIT,CFHIGHLANDER_DEFAULT_COMPONENT_GIT_LOOKUPin the same file/README):CFHIGHLANDER_COMPONENT_FETCH_RETRIES— number of retries (default3)CFHIGHLANDER_COMPONENT_FETCH_RETRY_DELAY— base delay in seconds for exponential backoff (default1, so delays are1s, 2s, 4s, ...)STDERR.rescue, so behavior after giving up is unchanged: logFailed to resolve component ...and returnnil. Callers see exactly the same failure mode as before, just after more attempts.sleep.findTemplateS3) and the default lookup URL resolution are untouched.Documented the two new env vars in the README's existing "Environment variables" section, next to
CFHIGHLANDER_AWS_RETRY_LIMIT.How
Added
spec/test_templatefinder_spec.rbwith two cases againstfindTemplateGit, stubbingGit.cloneandsleep(no real network calls or real waiting):sleepwas called with the expected backoff delays (1, then2).niland still logs the originalFailed to resolve component ...message, matching current behavior.Ran the full non-network spec suite locally (
bundle exec rspec spec/test_cloudformation_util_spec.rb spec/test_templatefinder_spec.rb) — 3 examples, 0 failures.spec/test_onestackorg_spec.rbwas left out of the local run since it needs live GitHub API access and clones several realtheonestack/hl-component-*repos; it isn't touched by this change.Also ran
ruby -con the changed file (syntax OK).