Skip to content

Commit 239066c

Browse files
committed
chore: redis cache type implemented
1 parent b3e6686 commit 239066c

8 files changed

Lines changed: 393 additions & 6 deletions

File tree

README.md

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Create a `zenith.json` in your project root including `projects` and `buildConfi
1313

1414
## Features
1515

16-
- **Local & Remote Caching:** Deploy faster using less bandwidth.
16+
- **Local & Remote Caching:** Deploy faster using less bandwidth. Supports local disk, S3-compatible remote storage, and Redis.
1717
- **No .git Required:** Ideal for team-based monorepo development.
1818
- **Versatile Commands:** Supports `build`, `test`, and more.
1919

@@ -26,6 +26,7 @@ Create a `zenith.json` in your project root including `projects` and `buildConfi
2626
- [zenith.json: What is it and why is it required?](#zenithjson-what-is-it-and-why-is-it-required)
2727
- [Required Parameters](#required-parameters)
2828
- [Environment Variables](#environment-variables)
29+
- [Cache types (usage)](#cache-types-usage)
2930
- [Params](#params)
3031
- [Optional Parameters](#optional-parameters)
3132
- [Debugging](#debugging)
@@ -46,11 +47,52 @@ pnpm add @jotforminc/zenith
4647

4748
From the terminal, run:
4849

49-
5050
```
5151
pnpm zenith --target=("build" | "test") --project=("all" | <project_name>)
5252
```
53+
5354
Target and project arguments are required for now. Without them, the tool will not work.
55+
56+
### Usage examples
57+
58+
Local cache (default — artifacts stored under `.cache` on disk):
59+
60+
```
61+
CACHE_TYPE=local pnpm zenith --target=build --project=all
62+
```
63+
64+
Remote S3 cache (requires S3 credentials; see [Local S3 with MinIO](#local-s3-with-minio) for a local setup):
65+
66+
```
67+
CACHE_TYPE=remote \
68+
S3_ACCESS_KEY=... \
69+
S3_SECRET_KEY=... \
70+
S3_BUCKET_NAME=... \
71+
S3_REGION=us-east-1 \
72+
pnpm zenith --target=build --project=all
73+
```
74+
75+
Redis cache (requires a running Redis server; see [Redis cache](#redis-cache)):
76+
77+
```
78+
CACHE_TYPE=redis \
79+
REDIS_URL=redis://127.0.0.1:6379 \
80+
pnpm zenith --target=build --project=all
81+
```
82+
83+
Hybrid modes (write to both backends; read from the first that hits):
84+
85+
```
86+
CACHE_TYPE=local-first pnpm zenith --target=build --project=all # local, then remote S3
87+
CACHE_TYPE=remote-first pnpm zenith --target=build --project=all # remote S3, then local
88+
```
89+
90+
Optional flags used often in CI or debugging:
91+
92+
```
93+
pnpm zenith --target=build --project=all --logLevel=3 --cache-format=zip
94+
```
95+
5496
## zenith.json: What is it and why is it required?
5597
Zenith looks for a file named "zenith.json" in the same folder where your root package.json file is. This file is used to determine the behavior of Zenith. It MUST include 'projects' and 'buildConfig' keys, and MAY include 'ignore' and 'appDirectories' keys. An example of usage is as follows.
5698
```json
@@ -116,7 +158,9 @@ Zenith looks for a file named "zenith.json" in the same folder where your root p
116158
The project uses several required environment variables and params. Without them, the tool will not work as intended.
117159
### Environment Variables
118160
```
119-
- CACHE_TYPE (string): One of ['local', 'remote', 'local-first', 'remote-first'], 'local' by default. If 'remote', S3 environment variables are required.
161+
- CACHE_TYPE (string): One of ['local', 'remote', 'redis', 'local-first', 'remote-first'], 'local' by default. If 'remote' or hybrid modes that include remote, S3 environment variables are required. If 'redis', REDIS_URL is required (defaults to redis://127.0.0.1:6379).
162+
- REDIS_URL (string): Redis connection URL when CACHE_TYPE=redis. Default: redis://127.0.0.1:6379.
163+
- REDIS_KEY_PREFIX (string): Prefix for all Redis cache keys when CACHE_TYPE=redis. Default: zenith:.
120164
- S3_ACCESS_KEY (string): Access key to be used to get objects from and write objects to the buckets.
121165
- S3_SECRET_KEY (string): Secret key to be used to get objects from and write objects to the buckets.
122166
- S3_BUCKET_NAME (string): Bucket name to be written and read from.
@@ -145,6 +189,36 @@ export S3_REGION=us-east-1
145189
```
146190
Stop MinIO: `yarn minio:down`.
147191

192+
### Redis cache
193+
194+
1. Install Redis if needed: `brew install redis`
195+
2. Start a local server: `yarn redis:up` (uses the `redis-server` binary on `127.0.0.1:6379`).
196+
- If you prefer Docker, use `docker run -d --name zenith-redis -p 6379:6379 redis:7-alpine` (requires Docker Desktop to be running).
197+
3. Point Zenith at Redis:
198+
199+
```
200+
export CACHE_TYPE=redis
201+
export REDIS_URL=redis://127.0.0.1:6379
202+
export REDIS_KEY_PREFIX=zenith:
203+
pnpm zenith --target=build --project=all
204+
```
205+
206+
Stop Redis: `yarn redis:down`.
207+
208+
Redis stores cache blobs as binary values keyed by the same paths used for S3/local storage (`zenith:` + `target/layoutHash/projectRoot/...`). Use a dedicated Redis instance or database index in shared environments.
209+
210+
### Cache types (usage)
211+
212+
| CACHE_TYPE | Where artifacts go | When to use |
213+
|------------|-------------------|-------------|
214+
| `local` | `.cache/` on disk (or `LOCAL_CACHE_PATH`) | Solo development, no shared cache needed |
215+
| `remote` | S3-compatible bucket | CI/CD teams sharing cache across machines |
216+
| `redis` | Redis server | Fast shared cache when you already run Redis; good for smaller/medium artifacts |
217+
| `local-first` | Local disk + S3 (read local first) | Dev machines with optional remote fallback |
218+
| `remote-first` | S3 + local disk (read remote first) | CI agents that also keep a local copy |
219+
220+
For `remote`, `local-first`, and `remote-first`, set the S3 variables listed above. For `redis`, set `REDIS_URL` (and optionally `REDIS_KEY_PREFIX`).
221+
148222
### Cache format benchmark (local MinIO)
149223

150224
After `yarn build` and `yarn minio:up`, with remote S3 env vars set:

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@jotforminc/zenith",
33
"packageManager": "yarn@1.22.21",
4-
"version": "3.2.3",
4+
"version": "3.3.0",
55
"description": "",
66
"main": "./build/index.js",
77
"files": [
@@ -36,6 +36,8 @@
3636
"tsc:w": "tsc -w",
3737
"minio:up": "node scripts/minio-up.cjs",
3838
"minio:down": "node scripts/minio-down.cjs",
39+
"redis:up": "node scripts/redis-up.cjs",
40+
"redis:down": "node scripts/redis-down.cjs",
3941
"bench:cache-formats": "node scripts/benchmark-cache-formats.cjs"
4042
},
4143
"keywords": [],
@@ -47,6 +49,7 @@
4749
"commander": "10.0.1",
4850
"extract-zip": "2.0.1",
4951
"jszip": "3.10.1",
52+
"redis": "^4.7.0",
5053
"tar": "7.5.11",
5154
"typescript": "^5.0.4",
5255
"voici.js": "^2.1.0",

scripts/redis-down.cjs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs/promises');
4+
const path = require('path');
5+
6+
const PID_PATH = path.join('/tmp', 'zenith-redis.pid');
7+
8+
const stopRedis = async () => {
9+
let pid = 0;
10+
try {
11+
pid = Number.parseInt((await fs.readFile(PID_PATH, 'utf8')).trim(), 10);
12+
} catch {
13+
process.stdout.write('Redis is not running (pid file missing).\n');
14+
return;
15+
}
16+
17+
if (Number.isInteger(pid) && pid > 0) {
18+
try {
19+
process.kill(pid, 'SIGTERM');
20+
process.stdout.write(`Stopped Redis process ${pid}.\n`);
21+
} catch {
22+
process.stdout.write(`Redis process ${pid} was not running.\n`);
23+
}
24+
}
25+
26+
await fs.rm(PID_PATH, { force: true });
27+
};
28+
29+
stopRedis().catch((error) => {
30+
process.stderr.write(`${error?.message || error}\n`);
31+
process.exit(1);
32+
});

scripts/redis-up.cjs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env node
2+
3+
const { spawn, spawnSync } = require('child_process');
4+
const fs = require('fs');
5+
const fsp = require('fs/promises');
6+
const path = require('path');
7+
8+
const REDIS_HOST = '127.0.0.1';
9+
const REDIS_PORT = 6379;
10+
11+
const DATA_DIR = path.join('/tmp', 'zenith-redis-data');
12+
const LOG_PATH = path.join('/tmp', 'zenith-redis.log');
13+
const PID_PATH = path.join('/tmp', 'zenith-redis.pid');
14+
15+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16+
17+
const isProcessRunning = (pid) => {
18+
if (!Number.isInteger(pid) || pid <= 0) return false;
19+
try {
20+
process.kill(pid, 0);
21+
return true;
22+
} catch {
23+
return false;
24+
}
25+
};
26+
27+
const pingRedis = () => {
28+
const result = spawnSync(
29+
'redis-cli',
30+
['-h', REDIS_HOST, '-p', `${REDIS_PORT}`, 'ping'],
31+
{ encoding: 'utf8' },
32+
);
33+
return result.stdout?.trim() === 'PONG';
34+
};
35+
36+
const waitForRedisReady = async () => {
37+
for (let i = 0; i < 60; i += 1) {
38+
if (pingRedis()) return;
39+
await sleep(500);
40+
}
41+
throw new Error('Redis did not become ready in time');
42+
};
43+
44+
const ensureRedisInstalled = () => {
45+
const server = spawnSync('redis-server', ['--version'], { stdio: 'ignore' });
46+
const cli = spawnSync('redis-cli', ['--version'], { stdio: 'ignore' });
47+
if (server.status !== 0 || cli.status !== 0) {
48+
throw new Error('Redis is not available in PATH. Install via `brew install redis`.');
49+
}
50+
};
51+
52+
const startRedis = async () => {
53+
ensureRedisInstalled();
54+
await fsp.mkdir(DATA_DIR, { recursive: true });
55+
56+
if (pingRedis()) {
57+
process.stdout.write(`Redis already running at redis://${REDIS_HOST}:${REDIS_PORT}\n`);
58+
return;
59+
}
60+
61+
let existingPid = 0;
62+
try {
63+
existingPid = Number.parseInt((await fsp.readFile(PID_PATH, 'utf8')).trim(), 10);
64+
} catch {
65+
existingPid = 0;
66+
}
67+
68+
if (!isProcessRunning(existingPid)) {
69+
const logFd = fs.openSync(LOG_PATH, 'a');
70+
const child = spawn(
71+
'redis-server',
72+
[
73+
'--port',
74+
`${REDIS_PORT}`,
75+
'--bind',
76+
REDIS_HOST,
77+
'--dir',
78+
DATA_DIR,
79+
'--save',
80+
'',
81+
],
82+
{
83+
detached: true,
84+
stdio: ['ignore', logFd, logFd],
85+
},
86+
);
87+
child.unref();
88+
await fsp.writeFile(PID_PATH, `${child.pid}\n`, 'utf8');
89+
}
90+
91+
await waitForRedisReady();
92+
93+
process.stdout.write(`Redis ready at redis://${REDIS_HOST}:${REDIS_PORT}\n`);
94+
};
95+
96+
startRedis().catch((error) => {
97+
process.stderr.write(`${error?.message || error}\n`);
98+
process.exit(1);
99+
});

src/classes/Cache/CacheFactory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Cacher from './Cacher';
22
import LocalCacher from './LocalCacher';
33
import RemoteCacher from './RemoteCacher';
4+
import RedisCacher from './RedisCacher';
45
import HybridCacher from './HybridCacher';
56
import { configManagerInstance } from '../../config';
67

@@ -12,6 +13,8 @@ export default class CacherFactory {
1213
return new LocalCacher();
1314
case 'remote':
1415
return new RemoteCacher();
16+
case 'redis':
17+
return new RedisCacher();
1518
case 'local-first':
1619
case 'remote-first':
1720
return new HybridCacher(cache) as Cacher;

0 commit comments

Comments
 (0)