Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,8 +598,57 @@ variables have an effect on leeway:
- `LEEWAY_CACHE_DIR`: Location of the local build cache. The directory does not have to exist yet.
- `LEEWAY_BUILD_DIR`: Working location of leeway (i.e. where the actual builds happen). This location will see heavy I/O which makes it advisable to place this on a fast SSD or in RAM.
- `LEEWAY_YARN_MUTEX`: Configures the mutex flag leeway will pass to yarn. Defaults to "network". See https://yarnpkg.com/lang/en/docs/cli/#toc-concurrency-and-mutex for possible values.
- `LEEWAY_NODE_MODULES_CACHE`: Directory to cache node_modules between yarn builds. When set, leeway restores node_modules from cache before `yarn install` and saves it after. Cache is keyed by package name and yarn.lock hash. See [Caching node_modules in CI](#caching-node_modules-in-ci) for usage.
- `LEEWAY_EXPERIMENTAL`: Enables experimental features

# Caching node_modules in CI

Yarn package builds can be slow due to `yarn install` downloading and extracting dependencies. The `LEEWAY_NODE_MODULES_CACHE` environment variable enables caching of `node_modules` directories between builds.

When enabled, leeway:
1. Before `yarn install`: Restores `node_modules` from cache if available
2. After `yarn install`: Saves `node_modules` to cache

The cache is keyed by package name and the first 12 characters of the yarn.lock SHA256 hash, so it automatically invalidates when dependencies change.

## GitHub Actions Example

```yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.leeway-node-modules
key: node-modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
node-modules-${{ runner.os }}-

- name: Build
env:
LEEWAY_NODE_MODULES_CACHE: ~/.leeway-node-modules
run: leeway build :my-yarn-package
```

## Performance Impact

| Scenario | Typical yarn install time |
|----------|---------------------------|
| No cache (cold) | 60-180 seconds |
| With node_modules cache | <5 seconds |

The cache directory structure is:
```
$LEEWAY_NODE_MODULES_CACHE/
component_package-name/ # package full name (slashes replaced with underscores)
a1b2c3d4e5f6/ # first 12 chars of yarn.lock hash
node_modules/
```

# OpenTelemetry Tracing

Leeway supports distributed tracing using OpenTelemetry for build performance visibility.
Expand Down
3 changes: 3 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ variables have an effect on leeway:
which makes it advisable to place this on a fast SSD or in RAM.
<light_blue>LEEWAY_YARN_MUTEX</> Configures the mutex flag leeway will pass to yarn. Defaults to "network".
See https://yarnpkg.com/lang/en/docs/cli/#toc-concurrency-and-mutex for possible values.
<light_blue>LEEWAY_NODE_MODULES_CACHE</> Directory to cache node_modules between yarn builds. When set, leeway restores
node_modules from cache before yarn install and saves it after. Cache is keyed by
package name and yarn.lock hash. Useful for CI to avoid repeated yarn installs.
<light_blue>LEEWAY_DEFAULT_CACHE_LEVEL</> Sets the default cache level for builds. Defaults to "remote".
<light_blue>LEEWAY_SLSA_CACHE_VERIFICATION</> Enables SLSA verification for cached artifacts (true/false).
<light_blue>LEEWAY_SLSA_SOURCE_URI</> Expected source URI for SLSA verification (github.com/owner/repo).
Expand Down
34 changes: 34 additions & 0 deletions pkg/leeway/build.go
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion, not blocking:
Consider adding unit tests to cover new feature. Perhaps in pkg/leeway/build_integration_test.go? Ona suggested peeking at TestYarnPackage_LinkDependencies_Integration for a reference.

To test node_modules caching, you could extend this pattern by:

  1. Setting LEEWAY_NODE_MODULES_CACHE env var to a temp directory
  2. Building the package once (should save to cache)
  3. Verifying the cache directory structure exists ($cache/pkg_name/hash[:12]/node_modules)
  4. Building again and checking logs for "Restoring node_modules from cache..."
  5. Modifying yarn.lock and rebuilding to verify cache invalidation (new hash directory)

The existing test at line 2427-2680 would be a good template - it already handles all the yarn package setup boilerplate.

Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ const (
// Defaults to "network".
EnvvarYarnMutex = "LEEWAY_YARN_MUTEX"

// EnvvarNodeModulesCache specifies a directory to cache node_modules between builds.
// When set, leeway will restore node_modules from cache before yarn install and save
// it back after. Cache is keyed by package name and yarn.lock hash.
EnvvarNodeModulesCache = "LEEWAY_NODE_MODULES_CACHE"

// EnvvarDockerExportToCache controls whether Docker images are exported to cache instead of pushed directly
EnvvarDockerExportToCache = "LEEWAY_DOCKER_EXPORT_TO_CACHE"

Expand Down Expand Up @@ -1892,11 +1897,40 @@ func (p *Package) buildYarn(buildctx *buildContext, wd, result string) (bld *pac
yarnMutex = "network"
}
yarnCache := filepath.Join(buildctx.BuildDir(), fmt.Sprintf("yarn-cache-%s", buildctx.buildID))

// node_modules caching: restore from cache before yarn install
nodeModulesCacheDir := os.Getenv(EnvvarNodeModulesCache)
var nodeModulesCachePath string
if nodeModulesCacheDir != "" {
// Compute cache key from yarn.lock hash
yarnLockPath := filepath.Join(wd, "yarn.lock")
yarnLockHash, err := computeSHA256(yarnLockPath)
if err != nil {
log.WithField("package", p.FullName()).WithError(err).Debug("cannot compute yarn.lock hash for node_modules cache")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion, not blocking:
This feels like we should bump to warn. If this were to happen with an existing yarn component, we'd want to surface it more easily, I think.

} else {
// Use package name and yarn.lock hash as cache key
// Replace slashes in package name with underscores for filesystem safety
safePkgName := strings.ReplaceAll(p.FullName(), "/", "_")
nodeModulesCachePath = filepath.Join(nodeModulesCacheDir, safePkgName, yarnLockHash[:12])

// Restore node_modules from cache if it exists
restoreCmd := fmt.Sprintf("if [ -d \"%s/node_modules\" ]; then echo \"Restoring node_modules from cache...\"; cp -a \"%s/node_modules\" ./node_modules; fi", nodeModulesCachePath, nodeModulesCachePath)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

TIL about -a flag with cp.

commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"sh", "-c", restoreCmd})
log.WithField("package", p.FullName()).WithField("cachePath", nodeModulesCachePath).Debug("node_modules cache enabled")
}
}

if len(cfg.Commands.Install) == 0 {
commands[PackageBuildPhasePull] = append(commands[PackageBuildPhasePull], []string{"yarn", "install", "--frozen-lockfile", "--mutex", yarnMutex, "--cache-folder", yarnCache})
} else {
commands[PackageBuildPhasePull] = append(commands[PackageBuildPhasePull], cfg.Commands.Install)
}

// node_modules caching: save to cache after yarn install
if nodeModulesCachePath != "" {
saveCmd := fmt.Sprintf("mkdir -p \"%s\" && rm -rf \"%s/node_modules\" && cp -a ./node_modules \"%s/node_modules\"", nodeModulesCachePath, nodeModulesCachePath, nodeModulesCachePath)
commands[PackageBuildPhasePull] = append(commands[PackageBuildPhasePull], []string{"sh", "-c", saveCmd})
}
if len(cfg.Commands.Build) == 0 {
commands[PackageBuildPhaseBuild] = append(commands[PackageBuildPhaseBuild], []string{"yarn", "build"})
} else {
Expand Down
Loading