@brutalsystems/dray 0.1.8 → 0.1.9

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 CHANGED
@@ -35,7 +35,7 @@ Requires `docker buildx`, `kubectl`, `git`, `aws`, and (for pilets/secrets) `sop
35
35
  dray # interactive menu
36
36
  dray list # registered repos + targets
37
37
  dray ship <repo>:<target> # deps?→build→push(SHA)→render+apply→rollout
38
- dray ship <repo> # all enabled workloads in the repo
38
+ dray ship <repo> # all enabled workloads in the repo (skips manual ones)
39
39
  dray apply <repo>:<target> # render + apply manifests only
40
40
  dray rollout <repo>:<target>
41
41
  dray status <repo> # running image SHA vs HEAD
@@ -72,8 +72,13 @@ Global flags: `--dry-run` (print the plan, run nothing), `--allow-dirty`
72
72
  // kind: deployment (rolled out) or cronjob (applied only)
73
73
  { "name": "app", "kind": "deployment", "image": "app",
74
74
  "manifests": [".k8s/deployment.yaml", ".k8s/service.yaml"], "stampImages": ["app"] },
75
- // disabled: kept in config but skipped (e.g. served elsewhere)
75
+ // disabled: kept in config but skipped (e.g. served elsewhere).
76
+ // Targeting it by name is an error.
76
77
  { "name": "legacy", "kind": "deployment", "image": "app", "manifests": ["..."], "disabled": true },
78
+ // manual: deployed only by `dray ship <repo>:<name>`, never by a bare-repo
79
+ // ship or --all. Use it to keep a workload out of CI (which ships the bare
80
+ // repo) while still deploying it on demand. Its image is skipped too.
81
+ { "name": "jobs", "kind": "deployment", "image": "jobs", "manifests": ["..."], "manual": true },
77
82
  // image-less: apply-only (third-party image lives literally in the manifest)
78
83
  { "name": "searxng", "kind": "deployment", "manifests": [".k8s/searxng.yaml"] }
79
84
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brutalsystems/dray",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Convention-driven multi-repo deploy orchestrator (forge for shipping)",
5
5
  "bin": {
6
6
  "dray": "bin/dray.js"
@@ -1,5 +1,5 @@
1
1
  const { loadGlobalConfig } = require('../../config/global'); const { allRepos } = require('../../config/registry');
2
- const { resolveTargets } = require('../../core/resolve'); const { runningImage } = require('../../primitives/kubectl'); const { currentSha } = require('../../primitives/git');
2
+ const { resolveTargets } = require('../../core/resolve'); const { runningImage } = require('../../primitives/kubectl'); const { currentSha, lsRemote } = require('../../primitives/git');
3
3
  module.exports = function registerStatus(program) {
4
4
  program.command('status [target]').description('Running image SHA vs HEAD').option('--all', 'all repos')
5
5
  .action(async (target, opts) => {
@@ -7,7 +7,12 @@ module.exports = function registerStatus(program) {
7
7
  const units = resolveTargets({ registry: allRepos(), globalDefaults: defaults, spec: opts.all || !target ? '--all' : target }).filter((u) => u.workload);
8
8
  for (const u of units) {
9
9
  const live = await runningImage({ workload: u.workload, kind: u.kind, context: u.defaults.context, namespace: u.defaults.namespace });
10
- const head = await currentSha(u.repoPath).catch(() => '?');
10
+ // A git-source image builds from its remote, not the repo `status` runs in —
11
+ // so compare against the remote HEAD, not the local (unrelated) repo's.
12
+ const src = u.image && u.image.source;
13
+ const head = src && src.git
14
+ ? await lsRemote({ git: src.git, ref: src.ref || 'HEAD' }).catch(() => '?')
15
+ : await currentSha(u.repoPath).catch(() => '?');
11
16
  const tag = live.includes(':') ? live.split(':').pop() : '(none)';
12
17
  console.log(`${u.repo}:${u.workload} running ${tag} ${tag === head ? 'up-to-date' : `behind (HEAD ${head})`}`);
13
18
  }
@@ -21,6 +21,14 @@ function validateRepoConfig(cfg) {
21
21
  if (!WORKLOAD_KINDS.has(w.kind)) e.push(`workload "${w.name}" kind must be deployment|cronjob`);
22
22
  if (w.image !== undefined && !imageNames.has(w.image)) e.push(`workload "${w.name}" references unknown image "${w.image}"`);
23
23
  if (!Array.isArray(w.manifests) || w.manifests.length === 0) e.push(`workload "${w.name}" needs a non-empty manifests list`);
24
+ // Both gate deployment on truthiness, so a JSON string ("false", "no") would
25
+ // silently skip the workload -- in `manual`'s case, silently drop it from CI.
26
+ // Fail loud instead of deploying something different from what was meant.
27
+ for (const flag of ['disabled', 'manual']) {
28
+ if (w[flag] !== undefined && typeof w[flag] !== 'boolean') {
29
+ e.push(`workload "${w.name}" ${flag} must be a boolean, got ${typeof w[flag]}`);
30
+ }
31
+ }
24
32
  }
25
33
  for (const s of cfg.secrets || []) {
26
34
  if (!s.name) e.push('secret missing name');
@@ -37,7 +37,12 @@ async function execute(steps, { dryRun = false, allowDirty = false, deps } = {})
37
37
  }
38
38
  const sha = await shaForUnit(u, d, shaCache, dryRun, allowDirty);
39
39
  if (u._clone && !clones.includes(u._clone)) clones.push(u._clone);
40
- const cwd = u._clone ? u._clone.dir : u.repoPath;
40
+ // Real builds use the clone dir. On dry-run we skip the clone, so show the
41
+ // git source (not the local repo `dray` was invoked from) — otherwise the
42
+ // printed command misleadingly implies the local Dockerfile is built.
43
+ const src = u.image && u.image.source;
44
+ const cwd = u._clone ? u._clone.dir
45
+ : (dryRun && src && src.git ? `<git ${src.git}@${src.ref || 'HEAD'} — cloned at build>` : u.repoPath);
41
46
  if (step.kind === 'deps') { if (d.depsCache.needsDepsRebuild(u.image, cwd, u.repo)) await d.docker.buildDeps(u.image, cwd, dryRun); }
42
47
  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
48
  else if (step.kind === 'push') {
@@ -38,6 +38,13 @@ function unitsForRepo(name, entry, globalDefaults, filter) {
38
38
  }
39
39
  continue;
40
40
  }
41
+ // Deployed only when explicitly targeted (dray ship repo:name). Skipped by a
42
+ // bare-repo ship and --all -- that is what CI runs, so `manual` keeps a
43
+ // workload out of the automatic pipeline while still allowing an on-demand
44
+ // deploy. Unlike `disabled`, targeting it by name is not an error. The image
45
+ // is skipped with it: the image-only fallback below only fires when NO
46
+ // workload claims the image, and a skipped manual workload still claims it.
47
+ if (w.manual && !filter) continue;
41
48
  mk(images.get(w.image), w);
42
49
  }
43
50
  for (const img of images.values()) {
@@ -2,6 +2,10 @@ const fs = require('node:fs'); const os = require('node:os'); const path = requi
2
2
  const { run } = require('./exec');
3
3
  async function currentSha(cwd) { const { stdout } = await run('git', ['rev-parse', '--short', 'HEAD'], { cwd, capture: true }); return stdout.trim(); }
4
4
  async function isDirty(cwd) { const { stdout } = await run('git', ['status', '--porcelain'], { cwd, capture: true }); return stdout.trim().length > 0; }
5
+ // The HEAD sha of a git-source image's REMOTE ref, without cloning — so `status`
6
+ // compares against the actual build source, not the local repo it happens to be
7
+ // invoked from. Short form to match `currentSha`.
8
+ async function lsRemote({ git, ref }) { const { stdout } = await run('git', ['ls-remote', git, ref], { capture: true }); return (stdout.trim().split(/\s+/)[0] || '').slice(0, 7); }
5
9
  async function cloneToTmp({ git, ref }) {
6
10
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dray-clone-'));
7
11
  // Works for branch/tag refs always; arbitrary commit SHAs require the remote
@@ -13,4 +17,4 @@ async function cloneToTmp({ git, ref }) {
13
17
  const sha = await currentSha(dir);
14
18
  return { dir, sha, cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) };
15
19
  }
16
- module.exports = { currentSha, isDirty, cloneToTmp };
20
+ module.exports = { currentSha, isDirty, cloneToTmp, lsRemote };