Skip to content

Feat add recursive directory support to push#1951

Draft
TerryHowe wants to merge 3 commits intooras-project:mainfrom
TerryHowe:feat-add-recursive-directory-support-to-push
Draft

Feat add recursive directory support to push#1951
TerryHowe wants to merge 3 commits intooras-project:mainfrom
TerryHowe:feat-add-recursive-directory-support-to-push

Conversation

@TerryHowe
Copy link
Copy Markdown
Member

@TerryHowe TerryHowe commented Dec 28, 2025

What this PR does / why we need it:

Recursive directory copy for oras push

Fixes: #1871

Signed-off-by: Terry Howe <terrylhowe@gmail.com>
@TerryHowe TerryHowe marked this pull request as draft December 28, 2025 17:36
@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 28, 2025

Codecov Report

❌ Patch coverage is 70.50000% with 118 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.09%. Comparing base (cdef683) to head (d1199e1).
⚠️ Report is 19 commits behind head on main.

Files with missing lines Patch % Lines
cmd/oras/root/push.go 33.33% 63 Missing and 3 partials ⚠️
internal/manifestutil/builder.go 78.10% 17 Missing and 13 partials ⚠️
internal/dir/walker.go 83.94% 12 Missing and 10 partials ⚠️

❌ Your patch check has failed because the patch coverage (70.50%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1951      +/-   ##
==========================================
- Coverage   87.22%   86.09%   -1.13%     
==========================================
  Files         143      147       +4     
  Lines        5540     5939     +399     
==========================================
+ Hits         4832     5113     +281     
- Misses        421      514      +93     
- Partials      287      312      +25     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

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

@TerryHowe TerryHowe force-pushed the feat-add-recursive-directory-support-to-push branch from c470325 to ed59da2 Compare December 28, 2025 17:46
Signed-off-by: Terry Howe <terrylhowe@gmail.com>
@TerryHowe TerryHowe force-pushed the feat-add-recursive-directory-support-to-push branch from ed59da2 to 1ade71a Compare December 28, 2025 17:56
Add comprehensive unit tests for the recursive push feature to
improve test coverage of cmd/oras/root/push.go.

Signed-off-by: Terry Howe <terrylhowe@gmail.com>
@lcarva
Copy link
Copy Markdown

lcarva commented Jan 9, 2026

Thanks for implementing this!

I'm having success with oras push --recursive. However, I'm getting a failures with oras pull.

Generate some file data:

mkdir -p sample/dirs-only/{assets,config}
mkdir -p sample/files-only
mkdir -p sample/mixed/{src,tests}

fortune > sample/dirs-only/assets/logo.png
fortune > sample/dirs-only/config/settings.json
fortune > sample/files-only/config.txt
fortune > sample/files-only/data.json
fortune > sample/files-only/readme.md
fortune > sample/mixed/package.json
fortune > sample/mixed/README.md
fortune > sample/mixed/src/main.js
fortune > sample/mixed/tests/test.js

That should create the following:

$ tree sample
sample
├── dirs-only
│   ├── assets
│   │   └── logo.png
│   └── config
│       └── settings.json
├── files-only
│   ├── config.txt
│   ├── data.json
│   └── readme.md
└── mixed
    ├── package.json
    ├── README.md
    ├── src
    │   └── main.js
    └── tests
        └── test.js

This is successful:

$ oras push --recursive localhost:5000/oras/sample:latest ./sample

On a different directory, this fails:

$ oras pull localhost:5000/oras/sample:latest
✓ Pulled      files-only/config.txt                                                                    61/61  B 100.00%   83µs
  └─ sha256:989523c961939b4ab17482eea045896c679dd5695498df91fcb1941c607371df                                                  
✓ Pulled      mixed/src/main.js                                                                      164/164  B 100.00%   93µs
  └─ sha256:2f37125c73db3133437db82348cace40c4532a4e24f880237d12fa98d5d2968d                                                  
✓ Pulled      files-only/data.json                                                                     25/25  B 100.00%  195µs
  └─ sha256:6e95b87f32d8b6ba1f82e1dd8e25731f986f4ab58b92d05d3e6c9466dbd223b7                                                  
✓ Pulled      mixed/README.md                                                                        228/228  B 100.00%   87µs
  └─ sha256:4025c62d58896ee69189054e7527a9521cde1d94f184719a6fd44a77380c73d7                                                  
✓ Pulled      mixed/package.json                                                                     140/140  B 100.00%   10µs
  └─ sha256:0fbac35a1bb9a5fe62859a5c4d673b0e9eb7e7a70f3da5aef7a09a5cd38b281a                                                  
✓ Pulled      mixed/tests/test.js                                                                    152/152  B 100.00%   54µs
  └─ sha256:6abc94092687a502683788c37db77f6a6471c268c3edef0917c5de64e58a77ba                                                  
✓ Pulled      files-only/readme.md                                                                   596/596  B 100.00%    5µs
  └─ sha256:b693e58040e276bc3f4bc3bb1307bacfa1db22c702c42e11a183f92311ca0b98                                                  
✓ Pulled      dirs-only/assets/logo.png                                                                54/54  B 100.00%   45µs
  └─ sha256:c4563c17afb4e4229a57ac01defb3c742c48844347d2eaa2fd9fc188c9b16269                                                  
✓ Pulled      src                                                                                    571/571  B 100.00%  122µs
  └─ sha256:9832ec6366b5e58602c09745eb3bb5d5b95ab512d3d3bbe19f8a28cfe992b05f                                                  
✓  mixed                                                                                             790/790  B 100.00%     0s
  └─ sha256:74e1558a5e2700f61983035b8bfa733185a588c92da1c997ef69deddb113b4bc                                                  
✓  files-only                                                                                      1022/1022  B 100.00%     0s
  └─ sha256:2ff8374f79f0e130eeb815c94628e3d3ff09ffa92d3026c66a941bff1c354164                                                  
Error: failed to create file /tmp/scratch/crazy-noether/mixed: open /tmp/scratch/crazy-noether/mixed: is a directory

I think the problem is related to the org.opencontainers.image.title annotation. Each sub-directory is getting recreated as an empty file at the root.

Comment thread cmd/oras/root/push.go
}

// Create a combined source for copying
combinedSource := contentutil.MultiReadOnlyTarget(memoryStore, store)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could just use the previously created union?


// MakeFileAnnotations creates annotations for a file descriptor.
// The title should be the filename for display purposes.
func MakeFileAnnotations(path, title string) map[string]string {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

title is unused. Intentional?

Comment thread internal/dir/walker.go
}

// ChunkFiles splits a list of nodes into chunks of at most maxSize.
func ChunkFiles(nodes []*Node, maxSize int) [][]*Node {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This doesn't seem to be used anywhere.

Comment thread internal/dir/walker.go
// IncludeEmpty includes empty directories in the tree.
IncludeEmpty bool
// ExcludePatterns are glob patterns for files/directories to exclude.
ExcludePatterns []string
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This makes sense, but it seems unreachable from the CLI. Intentional?

Comment thread internal/dir/walker.go

info, err := getFileInfo(entry, childAbsPath, opts.FollowSymlinks)
if err != nil {
// Skip files we can't stat (e.g., broken symlinks)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should this return an error instead? If I'm trying to upload a broken symlink, I'd rather know about it.

@github-actions
Copy link
Copy Markdown

This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 30 days.

@github-actions github-actions Bot added the stale Inactive issues or pull requests label Feb 24, 2026
@TerryHowe TerryHowe added keep open and removed stale Inactive issues or pull requests labels Feb 24, 2026
@TerryHowe TerryHowe added this to the v1.4.0 milestone Feb 24, 2026
robertream added a commit to robertream/oras that referenced this pull request May 1, 2026
…e push

When buildNode stored child Image Index descriptors in the parent index's
manifests list, it called maps.Copy to attach directory annotations — but
createIndex had already set ocispec.AnnotationTitle=<dirname> on the
returned descriptor, so maps.Copy preserved it.

oras pull treats any descriptor with AnnotationTitle as a named file and
calls metadataHandler.OnFilePulled, which tries to open/create the path.
When a directory with that name already exists on disk this fails with:
  "open <path>/<dirname>: is a directory"

Fix:
- createIndex no longer adds AnnotationTitle to its annotations or the
  returned descriptor
- buildNode assigns (replaces) directory annotations instead of merging,
  ensuring no stale AnnotationTitle can survive from the inner call

Adds TestBuildFromNode_noAnnotationTitleOnIndexDescriptors as a regression
test that reproduces the exact failure mode reported in PR oras-project#1951 review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
robertream added a commit to robertream/oras that referenced this pull request May 1, 2026
Adds three e2e specs under "pushing recursively with --recursive flag":

1. Push a nested directory tree and pull it back, verifying every file's
   content is preserved — this is the primary regression test for the
   "open <path>: is a directory" bug from PR oras-project#1951 review.

2. Error: --recursive with a file argument (not a directory).

3. Error: --recursive combined with --config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
robertream added a commit to robertream/oras that referenced this pull request May 6, 2026
…e push

When buildNode stored child Image Index descriptors in the parent index's
manifests list, it called maps.Copy to attach directory annotations — but
createIndex had already set ocispec.AnnotationTitle=<dirname> on the
returned descriptor, so maps.Copy preserved it.

oras pull treats any descriptor with AnnotationTitle as a named file and
calls metadataHandler.OnFilePulled, which tries to open/create the path.
When a directory with that name already exists on disk this fails with:
  "open <path>/<dirname>: is a directory"

Fix:
- createIndex no longer adds AnnotationTitle to its annotations or the
  returned descriptor
- buildNode assigns (replaces) directory annotations instead of merging,
  ensuring no stale AnnotationTitle can survive from the inner call

Adds TestBuildFromNode_noAnnotationTitleOnIndexDescriptors as a regression
test that reproduces the exact failure mode reported in PR oras-project#1951 review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Robert Ream <154010+robertream@users.noreply.github.com>
robertream added a commit to robertream/oras that referenced this pull request May 6, 2026
Adds three e2e specs under "pushing recursively with --recursive flag":

1. Push a nested directory tree and pull it back, verifying every file's
   content is preserved — this is the primary regression test for the
   "open <path>: is a directory" bug from PR oras-project#1951 review.

2. Error: --recursive with a file argument (not a directory).

3. Error: --recursive combined with --config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Robert Ream <154010+robertream@users.noreply.github.com>
robertream added a commit to robertream/oras that referenced this pull request May 6, 2026
…e push

When buildNode stored child Image Index descriptors in the parent index's
manifests list, it called maps.Copy to attach directory annotations — but
createIndex had already set ocispec.AnnotationTitle=<dirname> on the
returned descriptor, so maps.Copy preserved it.

oras pull treats any descriptor with AnnotationTitle as a named file and
calls metadataHandler.OnFilePulled, which tries to open/create the path.
When a directory with that name already exists on disk this fails with:
  "open <path>/<dirname>: is a directory"

Fix:
- createIndex no longer adds AnnotationTitle to its annotations or the
  returned descriptor
- buildNode assigns (replaces) directory annotations instead of merging,
  ensuring no stale AnnotationTitle can survive from the inner call

Adds TestBuildFromNode_noAnnotationTitleOnIndexDescriptors as a regression
test that reproduces the exact failure mode reported in PR oras-project#1951 review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Robert Ream <154010+robertream@users.noreply.github.com>
robertream added a commit to robertream/oras that referenced this pull request May 6, 2026
Adds three e2e specs under "pushing recursively with --recursive flag":

1. Push a nested directory tree and pull it back, verifying every file's
   content is preserved — this is the primary regression test for the
   "open <path>: is a directory" bug from PR oras-project#1951 review.

2. Error: --recursive with a file argument (not a directory).

3. Error: --recursive combined with --config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Robert Ream <154010+robertream@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add recursive directory support

2 participants