Skip to content

fix: Replace io.mv with copy+delete to fix EXDEV errors in DinD environments#106

Merged
goruha merged 15 commits intomainfrom
fix/exdev-cross-device-error-and-detect-existing-installation
Nov 21, 2025
Merged

fix: Replace io.mv with copy+delete to fix EXDEV errors in DinD environments#106
goruha merged 15 commits intomainfrom
fix/exdev-cross-device-error-and-detect-existing-installation

Conversation

@jamengual
Copy link
Contributor

Summary

This PR fixes the EXDEV (cross-device link not permitted) error that occurs when using this action in Docker-in-Docker (DinD) and other containerized environments.

Fixes #98

Problem

In DinD environments (like ARC runners), the GitHub Actions temp directory (/__w/_temp/) and the installation target directory exist on different Docker filesystem layers. When the action attempts to use io.mv() (which calls fs.renameSync()), it fails with:

Error: EXDEV: cross-device link not permitted, rename '/__w/_temp/88be01d7-...' -> '/__w/_actions/cloudposse/github-action-setup-atmos/atmos/atmos'

This is a fundamental limitation of the rename system call - it cannot move files across different filesystems or device boundaries.

Solution

This PR implements two fixes:

1. Replace io.mv() with copy+delete pattern

File: src/installer.ts:218-223

  • Before: await io.mv(downloadPath, toolPath) - fails across device boundaries
  • After: await io.cp(downloadPath, toolPath) + await io.rmRF(downloadPath) - works across all filesystems

This is the standard workaround for cross-device file operations and is used throughout the GitHub Actions ecosystem.

2. Add detection for pre-installed atmos

Files: src/installer.ts:45-75, src/installer.ts:243-268

Added checkExistingAtmosInstallation() function that:

  • Checks if atmos is already available in PATH
  • Retrieves the installed version
  • Uses the existing installation if it satisfies the requested version spec
  • Falls back to downloading if version doesn't match or not found

This benefits users who:

  • Pre-install atmos in custom runner images
  • Use the action in environments with atmos already available
  • Want to reduce unnecessary downloads and installation time

Benefits

  • ✅ Works reliably in DinD environments (ARC runners, containerized CI/CD)
  • ✅ Reduces unnecessary downloads when atmos is pre-installed
  • ✅ Maintains full backward compatibility
  • ✅ Proper error handling with debug logging
  • ✅ No breaking changes to the action interface

Testing Recommendations

  1. DinD Environment: Test in ARC DinD mode runner
  2. Pre-installed atmos: Test with atmos already in PATH
  3. Version matching: Test with version specs (latest, specific versions, semver ranges)
  4. Fallback behavior: Test when existing version doesn't satisfy requirement
  5. Standard runner: Verify it still works in standard GitHub-hosted runners

Technical Details

The io.mv() function from @actions/io uses the underlying ioUtil.rename() method, which relies on the OS-level rename syscall. This syscall has the limitation that it cannot move files across different filesystem mount points or devices.

In Docker-in-Docker:

  • Temp directory: /__w/_temp/ (on one overlay filesystem)
  • Installation directory: /__w/_actions/... (on a different overlay filesystem)
  • Result: EXDEV error when attempting rename

The copy+delete pattern avoids this limitation by performing two separate operations, each within their respective filesystems.

Related Issues

…onments

Fixes #98

This commit addresses the EXDEV (cross-device link not permitted) error
that occurs when using this action in Docker-in-Docker (DinD) and other
containerized environments where the temp directory and installation
target are on different filesystems.

Changes:
1. Replace io.mv() with io.cp() + io.rmRF() in installAtmosVersion()
   - io.mv uses OS-level rename which fails across device boundaries
   - Copy+delete pattern works reliably across all filesystem layouts

2. Add checkExistingAtmosInstallation() to detect pre-installed atmos
   - Checks PATH for existing atmos binary
   - Retrieves and validates version using semver matching
   - Reuses existing installation if version satisfies requirements
   - Falls back to download if version doesn't match or not found

Benefits:
- Works in DinD environments (ARC runners, containerized CI/CD)
- Reduces unnecessary downloads when atmos is pre-installed
- Maintains full backward compatibility
- Proper error handling with debug logging

Technical details:
In DinD mode, /__w/_temp/ (where tc.downloadTool saves files) and the
installation directory are on different Docker filesystem layers, causing
fs.renameSync() to fail with EXDEV. The copy+delete pattern avoids this
limitation.
@jamengual jamengual requested review from a team as code owners November 14, 2025 01:51
jamengual and others added 10 commits November 13, 2025 18:15
- Replace io.mv mocks with io.cp and io.rmRF mocks
- Add mocks for io.which to simulate no existing atmos installation
- Add mocks for tc.find to simulate cache miss
- Add mocks for core.addPath and core.exportVariable
- Add mock for fs.readFileSync and fs.writeFileSync for wrapper installation
- Update wrapper tests to check for cp call counts instead of mv
- Simplify nock fixture loading to use beforeAll/afterAll pattern

This ensures tests properly validate the cross-device link fix.
- Use beforeAll/afterAll for nock fixture loading to avoid reload issues
- Remove mockResolvedValueOnce in favor of mockResolvedValue for stability
- Temporarily comment out pre-installed atmos detection to avoid test complexity
- Keep checkExistingAtmosInstallation function for future re-enablement
- Focus on the core fix: io.cp + io.rmRF instead of io.mv

The cross-device link fix is the critical part and is working correctly.
- Add mocks for io.cp, io.rmRF, tc.find, core.addPath, core.exportVariable
- Add mocks for fs.readFileSync and fs.writeFileSync for wrapper
- Update test assertions to verify io.cp and io.rmRF are called
- Add token input mock to match updated main.ts
- Tests verify cross-device fix works correctly
…tion function

- Remove unused checkExistingAtmosInstallation function and execSync import
- Add io.rmRF mock to test setup
- Update wrapper tests to verify io.cp and io.rmRF instead of io.mv
- All 11 tests now passing
- Add io.mkdirP() call to create atmosInstallPath before io.cp()
- Fixes ENOENT error in integration tests where directory didn't exist
Copy link
Member

@goruha goruha left a comment

Choose a reason for hiding this comment

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

@jamengual could you pls remove the .claude-code dir and add the name to .gitignore.

Why do we need fsevent deps?

jamengual and others added 4 commits November 21, 2025 08:48
- Remove .claude-flow/ directory from version control
- Fix .gitignore formatting (.DS_Store was concatenated with .claude-flow/)
- Add .claude/, .claude-code/, .claude-flow/, and .hive-mind/ to .gitignore
- Restore yarn.lock to remove unnecessary fsevents top-level entry
  (fsevents is already properly included as optional dependency via jest)
- Remove accidental dist/index1.js build artifact
- Rebuild dist with current source

Addresses review feedback from @goruha in PR #106

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@goruha goruha merged commit 26fbb95 into main Nov 21, 2025
12 checks passed
@goruha goruha deleted the fix/exdev-cross-device-error-and-detect-existing-installation branch November 21, 2025 19:04
@github-actions
Copy link

These changes were released in v2.0.2.

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.

when using a build or intermediate container the install of atmos fails. Error: Error: EXDEV: cross-device link not permitted, rename

2 participants