@brutalsystems/dray 0.1.5 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/package.json +1 -1
- package/src/cli/index.js +1 -0
- package/src/config/registry.js +19 -1
- package/src/core/depsCache.js +17 -3
- package/src/core/engine.js +1 -1
- package/src/primitives/docker.js +11 -1
package/README.md
CHANGED
|
@@ -60,7 +60,11 @@ Global flags: `--dry-run` (print the plan, run nothing), `--allow-dirty`
|
|
|
60
60
|
// optional: inject --build-arg from an env file (e.g. VITE_* from .env.production),
|
|
61
61
|
// or from a sops-encrypted file decrypted in memory at build time (no plaintext on disk):
|
|
62
62
|
// "buildArgs": { "sopsEnvFile": "secrets.env", "prefix": "VITE_" }
|
|
63
|
-
"buildArgs": { "envFile": ".env.production", "prefix": "VITE_" }
|
|
63
|
+
"buildArgs": { "envFile": ".env.production", "prefix": "VITE_" },
|
|
64
|
+
// optional: also move a mutable <ecr>:latest tag onto every (non-dirty)
|
|
65
|
+
// push, so workloads that reference <ecr>:latest track the newest build
|
|
66
|
+
// without being re-shipped (e.g. rarely-shipped cronjobs sharing an image).
|
|
67
|
+
"latest": true },
|
|
64
68
|
// image from another repo (cloned to a tmp dir, built, pushed):
|
|
65
69
|
{ "name": "svc", "ecr": "svc", "source": { "git": "https://github.com/org/svc", "ref": "main" } }
|
|
66
70
|
],
|
|
@@ -91,3 +95,12 @@ Manifests reference managed images by a placeholder var — `${APP_IMAGE}`
|
|
|
91
95
|
substitutes `<ecr>:<gitSHA>` for that var across the listed files (container
|
|
92
96
|
image, env-var image refs, cronjob images — uniformly). A raw `kubectl apply`
|
|
93
97
|
of an unrendered manifest fails loud, so applies always go through dray.
|
|
98
|
+
|
|
99
|
+
An image with `"latest": true` additionally gets a mutable `<ecr>:latest` tag
|
|
100
|
+
moved onto each non-dirty push. This is for the opposite need: a workload that
|
|
101
|
+
should track the newest build of a shared image *without* being re-shipped
|
|
102
|
+
(e.g. a low-frequency cronjob sharing an image with a frequently-shipped
|
|
103
|
+
service). Reference `<ecr>:latest` literally in that manifest (with
|
|
104
|
+
`imagePullPolicy: Always`) instead of the `${..._IMAGE}` placeholder. You trade
|
|
105
|
+
away per-commit reproducibility/rollback for that workload — use it only where
|
|
106
|
+
that's the point.
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -3,6 +3,7 @@ async function run(argv) {
|
|
|
3
3
|
if (argv.length <= 2) { await require('./menu').menu(); return; }
|
|
4
4
|
const program = new Command();
|
|
5
5
|
program.name('dray').description('Convention-driven multi-repo deploy orchestrator');
|
|
6
|
+
program.version(require('../../package.json').version, '-v, --version', 'show dray version');
|
|
6
7
|
program.option('--dry-run', 'print planned commands, run nothing');
|
|
7
8
|
program.option('--allow-dirty', 'allow building from a dirty tree (tags :<sha>-dirty)');
|
|
8
9
|
for (const c of ['version', 'init', 'add', 'remove', 'reload', 'list',
|
package/src/config/registry.js
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
const fs = require('node:fs'); const path = require('node:path'); const { REGISTRY } = require('../constants');
|
|
2
|
+
const { loadRepoConfig } = require('./repo');
|
|
2
3
|
function loadRegistry(file = REGISTRY) { return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {}; }
|
|
3
4
|
function saveRegistry(file = REGISTRY, reg = {}) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(reg, null, 2)); }
|
|
4
5
|
function addRepo(file, { name, path: p, config }) { const r = loadRegistry(file); r[name] = { path: p, config, addedAt: new Date().toISOString() }; saveRegistry(file, r); return r[name]; }
|
|
5
6
|
function getRepo(file, name) { return loadRegistry(file)[name]; }
|
|
6
7
|
function removeRepo(file, name) { const r = loadRegistry(file); delete r[name]; saveRegistry(file, r); }
|
|
7
|
-
|
|
8
|
+
// The registry caches a copy of each repo's config. Auto-reload it when the
|
|
9
|
+
// live .dray/config.json is newer than that snapshot, so edits take effect
|
|
10
|
+
// without a manual `dray reload`. Falls back to the snapshot when the repo
|
|
11
|
+
// isn't checked out here (stat throws) or its config is momentarily invalid
|
|
12
|
+
// (loadRepoConfig throws) — a broken repo must not break `list`/`--all`.
|
|
13
|
+
function refreshEntry(entry) {
|
|
14
|
+
try {
|
|
15
|
+
const mtime = Math.floor(fs.statSync(path.join(entry.path, '.dray', 'config.json')).mtimeMs);
|
|
16
|
+
if (mtime <= (Date.parse(entry.addedAt) || 0)) return entry;
|
|
17
|
+
return { ...entry, config: loadRepoConfig(entry.path), addedAt: new Date(mtime).toISOString() };
|
|
18
|
+
} catch { return entry; }
|
|
19
|
+
}
|
|
20
|
+
function allRepos(file = REGISTRY) {
|
|
21
|
+
const reg = loadRegistry(file); let changed = false;
|
|
22
|
+
for (const [n, e] of Object.entries(reg)) { const f = refreshEntry(e); if (f !== e) { reg[n] = f; changed = true; } }
|
|
23
|
+
if (changed) saveRegistry(file, reg);
|
|
24
|
+
return reg;
|
|
25
|
+
}
|
|
8
26
|
module.exports = { loadRegistry, saveRegistry, addRepo, getRepo, removeRepo, allRepos };
|
package/src/core/depsCache.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
const fs = require('node:fs'); const path = require('node:path'); const crypto = require('node:crypto');
|
|
2
|
+
const { execFileSync } = require('node:child_process');
|
|
2
3
|
const { DRAY_HOME } = require('../constants');
|
|
3
|
-
|
|
4
|
+
|
|
5
|
+
// Whether the deps base image tag exists in the local Docker image store. A
|
|
6
|
+
// `docker system prune` can evict it while the lockfiles are unchanged — the hash
|
|
7
|
+
// check alone would then skip the rebuild and the main build fails at
|
|
8
|
+
// `FROM <depsImage.tag>: not found`.
|
|
9
|
+
function depsImagePresent(tag) {
|
|
10
|
+
try { execFileSync('docker', ['image', 'inspect', tag], { stdio: 'ignore' }); return true; }
|
|
11
|
+
catch { return false; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// `imagePresent` is injectable for tests; defaults to the real docker check.
|
|
15
|
+
function needsDepsRebuild(image, repoPath, repo, stateDir = path.join(DRAY_HOME, 'depshash'), imagePresent = depsImagePresent) {
|
|
4
16
|
if (!image.depsImage) return false;
|
|
5
17
|
const h = crypto.createHash('sha256');
|
|
6
18
|
for (const rel of image.depsImage.rebuildOn || []) {
|
|
@@ -11,7 +23,9 @@ function needsDepsRebuild(image, repoPath, repo, stateDir = path.join(DRAY_HOME,
|
|
|
11
23
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
12
24
|
const file = path.join(stateDir, `${repo}__${image.name}.depshash`);
|
|
13
25
|
const prev = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
14
|
-
|
|
26
|
+
// Rebuild when the lockfiles changed OR the cached deps image tag is gone.
|
|
27
|
+
const present = !image.depsImage.tag || imagePresent(image.depsImage.tag);
|
|
28
|
+
if (prev === digest && present) return false;
|
|
15
29
|
fs.writeFileSync(file, digest); return true;
|
|
16
30
|
}
|
|
17
|
-
module.exports = { needsDepsRebuild };
|
|
31
|
+
module.exports = { needsDepsRebuild, depsImagePresent };
|
package/src/core/engine.js
CHANGED
|
@@ -42,7 +42,7 @@ async function execute(steps, { dryRun = false, allowDirty = false, deps } = {})
|
|
|
42
42
|
else if (step.kind === 'build') await d.docker.buildImage({ ecrUri: u.repoUri, sha, dockerfile: u.image.dockerfile, context: u.image.context || '.', platform: u.defaults.platform, cwd, dryRun, buildArgs: await d.buildArgs.resolveBuildArgs(u.image, cwd, { dryRun }) });
|
|
43
43
|
else if (step.kind === 'push') {
|
|
44
44
|
if (!loggedIn) { await d.docker.ecrLogin({ account: u.defaults.account, region: u.defaults.region, profile: u.defaults.profile, dryRun }); await d.docker.ensureRepo({ ecr: u.image.ecr, account: u.defaults.account, region: u.defaults.region, profile: u.defaults.profile, dryRun }); loggedIn = true; }
|
|
45
|
-
await d.docker.pushImage({ ecrUri: u.repoUri, sha, dryRun });
|
|
45
|
+
await d.docker.pushImage({ ecrUri: u.repoUri, sha, latest: !!u.image.latest, dryRun });
|
|
46
46
|
} else if (step.kind === 'apply') {
|
|
47
47
|
const vars = {}; for (const s of u.stamp) vars[s.var] = `${s.repoUri}:${sha}`;
|
|
48
48
|
const files = d.render.renderManifests(u.manifests, vars, u.repoPath, { dryRun });
|
package/src/primitives/docker.js
CHANGED
|
@@ -6,7 +6,17 @@ function buildImage({ ecrUri, sha, dockerfile, context, platform, cwd, dryRun, b
|
|
|
6
6
|
args.push('--load', context);
|
|
7
7
|
return _run('docker', args, { cwd, dryRun });
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
// Push the immutable :<sha> tag. When the image opts into `latest: true`, also
|
|
10
|
+
// move a mutable :latest tag onto this build so workloads that reference
|
|
11
|
+
// <ecr>:latest (e.g. rarely-shipped cronjobs) track the newest build without
|
|
12
|
+
// being re-shipped. Never advance :latest for a dirty (`-dirty`) build.
|
|
13
|
+
async function pushImage({ ecrUri, sha, latest = false, dryRun }) {
|
|
14
|
+
await _run('docker', ['push', `${ecrUri}:${sha}`], { dryRun });
|
|
15
|
+
if (latest && !String(sha).endsWith('-dirty')) {
|
|
16
|
+
await _run('docker', ['tag', `${ecrUri}:${sha}`, `${ecrUri}:latest`], { dryRun });
|
|
17
|
+
await _run('docker', ['push', `${ecrUri}:latest`], { dryRun });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
function buildDeps(image, cwd, dryRun) {
|
|
11
21
|
const d = image.depsImage;
|
|
12
22
|
return _run('docker', ['buildx', 'build', '--platform', 'linux/arm64', '-f', d.dockerfile, '-t', d.tag, '--load', '.'], { cwd, dryRun });
|