Skip to content

feat(imports): cache remote stack-import clones (dedup + opt-in TTL)#2571

Merged
Andriy Knysh (aknysh) merged 6 commits into
mainfrom
osterman/remote-import-clone-caching
Jun 6, 2026
Merged

feat(imports): cache remote stack-import clones (dedup + opt-in TTL)#2571
Andriy Knysh (aknysh) merged 6 commits into
mainfrom
osterman/remote-import-clone-caching

Conversation

@osterman
Copy link
Copy Markdown
Member

@osterman Erik Osterman (Cloud Posse) (osterman) commented Jun 5, 2026

what

  • Clone each remote (Git) stack-import source repository at most once per Atmos invocation instead of once per import — all subdir imports of the same repo now resolve from a single shared clone (within-run dedup, spanning both describe affected passes).
  • Add an opt-in ttl to reuse the cloned source across runs until it expires: per-import (ttl: in the import map form) and a global imports.ttl default in atmos.yaml. With no ttl, the source refreshes once per run so mutable refs like ?ref=main stay fresh.
  • Wire the default git-subdir resolve path through the existing ensureSourceDir, add per-session fetch tracking + TTL freshness (timestamp persisted in the .atmos-source-ready marker), and extract a shared duration.IsExpired/IsZeroTTL that the source provisioner now reuses.
  • Update JSON schemas, add unit tests, document "Caching Remote Imports" in stacks/imports.mdx, add a changelog blog post, and add a roadmap milestone.

why

  • For hub-and-spoke repos pulling a shared catalog via remote imports, atmos describe affected was re-cloning the hub repo once per import (~68–87×/run, ~7–11 min total), and a warm actions/cache of ~/.cache/atmos/stack-imports/ was ignored because the subdir path re-cloned unconditionally.
  • Within-run dedup collapses those clones to one per repo (the ~80% win, no staleness risk); the opt-in ttl lets CI reuse the clone across runs (warm cache skips the clone entirely) while keeping mutable refs fresh by default. Shallow clones (depth=1) were already in use — the win is not re-cloning the same repo repeatedly.

references

  • Cached sources live under the XDG cache dir (~/.cache/atmos/stack-imports/, honoring XDG_CACHE_HOME).
  • Builds on the source-provisioning TTL mechanism (pkg/duration, pkg/provisioner/source).
  • Changelog: website/blog/2026-06-05-faster-remote-stack-imports.mdx

Summary by CodeRabbit

  • New Features

    • Per-import ttl and global imports.ttl for optional cross-run caching of remote stack imports.
    • Each unique remote Git source is cloned at most once per invocation and shared across nested imports.
    • Improved cache freshness semantics, including explicit zero-ttl behavior.
  • Documentation

    • Added caching guide, TTL examples, XDG cache guidance, and a blog post.
  • Tests

    • Added tests for TTL parsing/expiration and remote import caching behavior.

Clone each remote (git) stack-import source repo at most once per Atmos
invocation instead of once per import, and add an opt-in `ttl` to reuse
the clone across runs (so a warm CI cache can skip the re-clone).

- Wire the default git-subdir resolve path through ensureSourceDir so all
  subdir imports of the same repo share a single clone (within-run dedup,
  spanning both describe-affected passes via the global importer).
- Add per-session fetch tracking + TTL-based cross-run freshness to
  ensureSourceDir; persist UpdatedAt in the .atmos-source-ready marker.
  No ttl = refresh once per run, so mutable refs (?ref=main) stay fresh.
- Expose `ttl` per-import (StackImport.TTL) and as a global imports.ttl
  default; thread it through ResolveRemoteImportNested.
- Extract shared duration.IsExpired/IsZeroTTL; source provisioner now
  delegates to them.
- Update JSON schemas, add tests, docs, changelog post, and roadmap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@atmos-pro
Copy link
Copy Markdown
Contributor

atmos-pro Bot commented Jun 5, 2026

Tip

Atmos Pro  

No affected stacks workflow was detected for this pull request.
If this is expected, no action is needed.
Learn More. Ask AI.

@osterman Erik Osterman (Cloud Posse) (osterman) added the minor New features that do not break anything label Jun 5, 2026
@github-actions github-actions Bot added the size/m Medium size PR label Jun 5, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 5, 2026

Dependency Review

✅ No vulnerabilities or license issues found.

Scanned Files

None

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 5, 2026

Lost in the diff? Review this PR in Change Stack to follow the change map from intent to exact ranges.

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ab666815-6f1e-45d7-baca-5980718a7032

📥 Commits

Reviewing files that changed from the base of the PR and between 9846a5a and ea87576.

📒 Files selected for processing (6)
  • internal/exec/stack_processor_utils.go
  • pkg/schema/schema.go
  • pkg/stack/imports/remote.go
  • pkg/stack/imports/remote_cache_test.go
  • pkg/stack/imports/remote_nested.go
  • website/src/data/roadmap.js
✅ Files skipped from review due to trivial changes (1)
  • website/src/data/roadmap.js
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/exec/stack_processor_utils.go
  • pkg/schema/schema.go
  • pkg/stack/imports/remote_nested.go
  • pkg/stack/imports/remote.go

📝 Walkthrough

Walkthrough

Remote stack imports now support configurable caching via a new ttl field (per-import or global default). Sources are cloned once per run and deduplicated across subdir imports. Persisted metadata enables cross-run reuse when TTL is set and fresh; without TTL, sources refresh each run. Integration spans schemas, duration helpers, remote import resolvers, stack processor, and tests.

Changes

Remote Stack Import Caching with TTL

Layer / File(s) Summary
Schema and configuration types for TTL support
pkg/schema/schema.go, pkg/datafetcher/schema/atmos/manifest/1.0.json, pkg/datafetcher/schema/config/global/1.0.json, pkg/datafetcher/schema/stacks/stack-config/1.0.json
AtmosConfiguration gains a top-level imports section with default TTL, and StackImport adds an optional per-import TTL field. JSON schemas extend the import definition with ttl and nested_imports properties.
TTL expiration and zero-check utility functions
pkg/duration/duration.go, pkg/duration/duration_test.go
New helpers IsZeroTTL(ttl string) bool and IsExpired(updatedAt time.Time, ttl string) (bool, error) centralize TTL evaluation; tests cover zero formats, expiry math, and invalid TTL handling.
Provisioner source cache refactor to use shared TTL helpers
pkg/provisioner/source/provision_hook.go
isSourceCacheExpired now delegates to duration.IsExpired and isZeroTTL delegates to duration.IsZeroTTL, centralizing TTL logic instead of inline parsing and comparisons.
Remote importer refactoring with per-run dedupe
pkg/stack/imports/remote.go
RemoteImporter adds sessionFetched map to track per-invocation source clones under sourceMu. Resolve delegates to internal resolve(uri, ttl), threading TTL into resolveGitSubdir. Subdir resolver now uses shared clones from ensureSourceDir instead of fresh clones per import; ClearCache also resets sessionFetched.
Nested import TTL-aware cross-run caching
pkg/stack/imports/remote_nested.go
Replaces simple ready markers with JSON-persisted sourceMetadata tracking UpdatedAt. Cache reuse is determined by sourceCacheFresh using duration.IsExpired; within-run dedupe is unconditional. ResolveRemoteImportNested signature updated to accept and forward ttl parameter.
Stack processor TTL resolution and wiring
internal/exec/stack_processor_utils.go
Stack processor computes importTTL from per-import TTL with fallback to global Imports.TTL, logs it, and passes it to ResolveRemoteImportNested.
Remote import caching tests
pkg/stack/imports/remote_cache_test.go
Comprehensive test coverage validates per-run dedupe (two subdir imports trigger one fetch), cross-run reuse (TTL-fresh cache is reused, expired/no-TTL triggers refresh), distinct-source isolation, and sourceCacheFresh predicate with mocked downloader and file-backed cache.
User documentation, blog post, and roadmap
website/blog/2026-06-05-faster-remote-stack-imports.mdx, website/docs/stacks/imports.mdx, website/src/data/roadmap.js
New blog post explains per-run clone deduplication and cross-run TTL-based reuse with configuration examples. Docs section covers caching behavior, XDG cache directory, CI integration, and schema field reference. Roadmap milestone recorded.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • cloudposse/atmos#2528: Extends nested remote resolver by threading a per-import TTL through ResolveRemoteImportNested and its call sites.
  • cloudposse/atmos#2037: Introduced remote stack import flow that this PR extends with TTL-aware caching.
  • cloudposse/atmos#2138: Related TTL cache-expiration work in the provisioner that this PR aligns with via shared duration helpers.

Suggested reviewers

  • aknysh
  • kevcube
  • nitrocode
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(imports): cache remote stack-import clones (dedup + opt-in TTL)' directly and clearly summarizes the main change: adding caching with deduplication and optional TTL for remote stack imports.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch osterman/remote-import-clone-caching

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
website/src/data/roadmap.js (1)

323-323: ⚡ Quick win

Milestone structure looks solid.

The new "Remote import source caching" entry includes all required fields—status, quarter, changelog, docs—and the description accurately reflects the within-run dedup and opt-in TTL behavior. Consider adding pr: 2571 to link directly to this pull request for traceability.

➕ Optional: add PR link
-        { label: 'Remote import source caching (clone once + TTL)', status: 'shipped', quarter: 'q2-2026', changelog: 'faster-remote-stack-imports', docs: '/stacks/imports#caching-remote-imports', description: '...' },
+        { label: 'Remote import source caching (clone once + TTL)', status: 'shipped', quarter: 'q2-2026', pr: 2571, changelog: 'faster-remote-stack-imports', docs: '/stacks/imports#caching-remote-imports', description: '...' },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/src/data/roadmap.js` at line 323, Add a pr field linking the PR
number to the roadmap entry by updating the object with label 'Remote import
source caching (clone once + TTL)' to include pr: 2571 (e.g., add pr: 2571
alongside status, quarter, changelog, docs, description, benefits) so the
roadmap row references the originating pull request for traceability.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/duration/duration.go`:
- Around line 182-184: The zero-value TTL matching in the switch on ttl fails
for inputs with surrounding whitespace; trim ttl before comparing by calling
strings.TrimSpace on the input used in the switch (e.g., trim the ttl variable
at the start of the function or just before the switch in the duration
package/function that contains the switch), then perform the existing
comparisons ("0","0s","0m","0h","0d") against the trimmed value so values like "
0s " are recognized as zero TTL.

In `@website/src/data/roadmap.js`:
- Line 297: The progress for the extensibility initiative was decremented
incorrectly when a milestone was marked shipped; update the extensibility
object's progress property (currently `progress: 83`) to reflect the shipped
milestone (e.g., `progress: 85`) or, better, recalculate progress by counting
shipped entries in the extensibility.milestones array divided by total
milestones and writing that computed value back to the extensibility.progress
field; locate the `extensibility` object and its `progress` property in
roadmap.js and either adjust the literal or implement the recalculation logic
that derives progress from `milestones`.

---

Nitpick comments:
In `@website/src/data/roadmap.js`:
- Line 323: Add a pr field linking the PR number to the roadmap entry by
updating the object with label 'Remote import source caching (clone once + TTL)'
to include pr: 2571 (e.g., add pr: 2571 alongside status, quarter, changelog,
docs, description, benefits) so the roadmap row references the originating pull
request for traceability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4f6bf23e-9434-49bc-9e47-5bee61cef71f

📥 Commits

Reviewing files that changed from the base of the PR and between 860a0ec and df62013.

📒 Files selected for processing (13)
  • internal/exec/stack_processor_utils.go
  • pkg/datafetcher/schema/atmos/manifest/1.0.json
  • pkg/datafetcher/schema/config/global/1.0.json
  • pkg/datafetcher/schema/stacks/stack-config/1.0.json
  • pkg/duration/duration.go
  • pkg/provisioner/source/provision_hook.go
  • pkg/schema/schema.go
  • pkg/stack/imports/remote.go
  • pkg/stack/imports/remote_cache_test.go
  • pkg/stack/imports/remote_nested.go
  • website/blog/2026-06-05-faster-remote-stack-imports.mdx
  • website/docs/stacks/imports.mdx
  • website/src/data/roadmap.js

Comment thread pkg/duration/duration.go Outdated
Comment thread website/src/data/roadmap.js Outdated
- duration.IsZeroTTL now trims whitespace before zero-value matching, so
  " 0s " is recognized as a zero TTL (consistent with Parse/ParseDuration).
  Add TestIsZeroTTL (with whitespace regression cases) and TestIsExpired.
- roadmap: the "Custom hooks with built-in kinds" milestone (#2482) was
  fully shipped (merged df2c7bd, changelog custom-hooks) but still marked
  in-progress; flip it to shipped. Extensibility progress recomputed to
  26 shipped / 30 total = 87 (was an inaccurate 83).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/duration/duration_test.go (1)

185-213: 💤 Low value

Consider adding an exact boundary test case.

The current tests have comfortable margins (1 minute within 1 hour, 2 hours beyond 1 hour). Adding a case where updatedAt is exactly at the TTL boundary would explicitly verify the > comparison behavior.

📝 Suggested additional test case
 		{name: "stale entry beyond TTL", updatedAt: now.Add(-2 * time.Hour), ttl: "1h", wantExpired: true},
+		{name: "exactly at TTL boundary", updatedAt: now.Add(-1 * time.Hour), ttl: "1h", wantExpired: false},
 		{name: "invalid TTL returns error", updatedAt: now, ttl: "nonsense", wantErr: true},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/duration/duration_test.go` around lines 185 - 213, Add a boundary test
case in TestIsExpired that checks updatedAt exactly equals the TTL duration:
create a test entry named "exact TTL boundary" with updatedAt set to
now.Add(-1*time.Hour) and ttl "1h", then call IsExpired and assert the expected
boolean for the current implementation (i.e., whether equality is considered
expired or not); place this new case in the tests slice alongside the others in
the TestIsExpired function to explicitly verify IsExpired's comparison behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/duration/duration_test.go`:
- Around line 185-213: Add a boundary test case in TestIsExpired that checks
updatedAt exactly equals the TTL duration: create a test entry named "exact TTL
boundary" with updatedAt set to now.Add(-1*time.Hour) and ttl "1h", then call
IsExpired and assert the expected boolean for the current implementation (i.e.,
whether equality is considered expired or not); place this new case in the tests
slice alongside the others in the TestIsExpired function to explicitly verify
IsExpired's comparison behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eeadd4f4-e17c-44a3-bdc7-e547bf68ef78

📥 Commits

Reviewing files that changed from the base of the PR and between df62013 and 45117a7.

📒 Files selected for processing (3)
  • pkg/duration/duration.go
  • pkg/duration/duration_test.go
  • website/src/data/roadmap.js
✅ Files skipped from review due to trivial changes (1)
  • website/src/data/roadmap.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/duration/duration.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 5, 2026
Add a warning admonition to the Remote Imports docs: a remote stack
import resolves configuration only, not the referenced component's
Terraform/Helmfile root module. describe/list/validate work, but
plan/apply fails with "component not found" unless the component is
materialized via `source:` (JIT source provisioning) or `atmos vendor pull`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 5, 2026
…ection

The new global `imports` config section renders as `"imports": {}` in
`atmos describe config` JSON output (encoding/json does not honor omitempty
for empty structs, unlike yaml.v3). Regenerate the two affected golden
snapshots so Acceptance Tests pass on all platforms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 5, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 5, 2026

Codecov Report

❌ Patch coverage is 92.78351% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.91%. Comparing base (657a898) to head (ea87576).

Files with missing lines Patch % Lines
pkg/stack/imports/remote_nested.go 87.71% 4 Missing and 3 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2571      +/-   ##
==========================================
+ Coverage   78.87%   78.91%   +0.03%     
==========================================
  Files        1201     1201              
  Lines      115958   116016      +58     
==========================================
+ Hits        91464    91555      +91     
+ Misses      19435    19402      -33     
  Partials     5059     5059              
Flag Coverage Δ
unittests 78.91% <92.78%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
internal/exec/stack_processor_utils.go 82.36% <100.00%> (+0.04%) ⬆️
pkg/duration/duration.go 100.00% <100.00%> (ø)
pkg/provisioner/source/provision_hook.go 85.51% <100.00%> (ø)
pkg/schema/schema.go 87.70% <ø> (ø)
pkg/stack/imports/remote.go 92.48% <100.00%> (+0.78%) ⬆️
pkg/stack/imports/remote_nested.go 88.96% <87.71%> (+5.94%) ⬆️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mergify
Copy link
Copy Markdown

mergify Bot commented Jun 6, 2026

💥 This pull request now has conflicts. Could you fix it Erik Osterman (Cloud Posse) (@osterman)? 🙏

@mergify mergify Bot added the conflict This PR has conflicts label Jun 6, 2026
Andriy Knysh (aknysh) and others added 2 commits June 5, 2026 21:07
Resolve conflict in pkg/stack/imports/resolveGitSubdir: main's auth fix
(#2568) added broker.EnsureCredentials before detectGitSource, but this
branch refactored resolveGitSubdir to delegate cloning to ensureSourceDir.
Port the credential-provisioning fix into ensureSourceDir (remote_nested.go),
immediately before its detectGitSource call — the spot where the detect now
runs — so both the clone-caching feature and the Atmos Pro github/sts broker
ordering are preserved. Moved the broker/context imports accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests for the clone-caching paths that were uncovered:
- ResolveRemoteImportNested public wrapper, exercised through the global
  importer with a mock downloader (consuming globalImporterOnce so the
  injected importer is not overwritten), including within-run source dedup.
- readSourceMetadata error/validation paths: missing marker, malformed
  JSON, legacy marker without a timestamp, and a valid marker.

Raises pkg/stack/imports coverage 93.3% -> 94.7% (readSourceMetadata to
100%, ResolveRemoteImportNested 0% -> 80%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aknysh Andriy Knysh (aknysh) merged commit 01b3c60 into main Jun 6, 2026
65 checks passed
@aknysh Andriy Knysh (aknysh) deleted the osterman/remote-import-clone-caching branch June 6, 2026 03:01
@atmos-pro
Copy link
Copy Markdown
Contributor

atmos-pro Bot commented Jun 6, 2026

Tip

Atmos Pro  

No affected stacks workflow was detected for this pull request.
If this is expected, no action is needed.
Learn More. Ask AI.

Erik Osterman (Cloud Posse) (osterman) added a commit that referenced this pull request Jun 6, 2026
Shipped roadmap milestones require both changelog and pr; the merged
remote-import-caching entry (from #2571) was missing pr. Flagged by CodeRabbit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 7, 2026

These changes were released in v1.221.0-rc.6.

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

Labels

minor New features that do not break anything size/m Medium size PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants