Skip to content

Run commands concurrently with --jobs, ship as 0.5.0 - #2

Merged
aa8y merged 4 commits into
masterfrom
parallel-execution
Aug 10, 2026
Merged

aa8y merged 4 commits into
masterfrom
parallel-execution

Conversation

@aa8y

@aa8y aa8y commented Aug 10, 2026

Copy link
Copy Markdown
Owner

dave has always executed its rendered commands strictly serially — one for…of await
loop. For a manifest like docker-dataset's (~105 buildable tags), that means every
build, test, and push runs one at a time. This PR adds opt-in parallelism plus the
manifest features needed to skip alias tags, and trims the published package.

New flags

  • --jobs / -j N — run up to N commands of the same type concurrently.
    Guarantees: types stay strictly ordered (build → test → structure-test → push,
    hard barrier between them); at -j 1 (the default) behavior is byte-identical to
    0.4.1 with live-streaming output; at -j > 1 each command's output is buffered
    and printed as one contiguous block when it finishes — never interleaved.
  • --keep-going / -k — on failure, remaining commands of the same type still
    run; later types never start; all failures are summarized and the exit code is
    non-zero.

The pool path uses spawn with self-managed pipe buffering (uncapped), so the
exec() maxBuffer cliff that 79e19e0 fixed cannot regress.

New manifest features

  • Empty-render skip — a template that renders to only whitespace for a tag
    produces no command. Wrapping a template in {{^retagFrom}}…{{/retagFrom}} lets
    alias tags (e.g. latest) skip build/test/push entirely.
  • structureTest: false — per-tag/per-context opt-out from inherited
    structure-test configs (previously configs always concatenated down).
  • New exported getCommandsByType(); getCommands() is reimplemented on top of
    it, output unchanged.

Packaging

.npmignore is replaced with a "files" allowlist. The published tarball drops
from 9.5 MB to 12.7 kB (a decorative 9.3 MB GIF was shipping in every install).
A LICENSE file is added (package.json always declared MIT; the tarball now
actually carries the text).

Compatibility

Fully backward compatible: no flags → same serial, live-streaming execution as
0.4.1. Two minor behavior notes (also in CHANGELOG.md): an empty rendered template
no longer runs sh -c '', and invalid flag values print the error without the
full usage dump.

Testing

  • 108 tests (was 80), coverage at 100% statements/functions/lines.
  • Concurrency bound proven via a counter-file test; output contiguity proven by
    running dave as a child process at -j 3; type barrier and keep-going covered
    by dedicated fixtures.
  • Timing sanity: 8 tags × sleep 1 → 8.2 s at -j 1, 2.1 s at -j 4.
  • npm pack --dry-run: 6 files, 12.7 kB.

After merging: tag v0.5.0 and npm publish — the docker-dataset CI redesign
pins DAVE_VERSION: 0.5.0 and must not merge before the package is live.

🤖 Generated with Claude Code

aa8y and others added 4 commits August 9, 2026 20:12
Three additive changes to lib/manifest.js, all of which keep getCommands()
output byte-identical (its existing expectations pass untouched):

- getCommandsByType() walks contexts × tags exactly as getCommands() does but
  returns ordered [type, commands] pairs, dropping types which produced no
  command. getCommands() is now just its flattening, so the two cannot drift.
  Commands within a type are independent of each other while types are not
  (nothing is tested before it is built), which is the grouping a parallel
  executor needs — that follow-up will consume this.

- A template which renders to nothing (trimmed) no longer emits a command;
  previously it produced an empty string that ran as `sh -c ''`. This lets a
  manifest wrap a template in `{{^alias}}…{{/alias}}` so tags which are pure
  retags of another image are skipped for that type while still being pushed.

- `structureTest: false` (and its structure_test/structure-test aliases) is an
  explicit opt-out from the parent → child config concatenation, which until
  now no tag could escape. As a child it wins outright; as a parent it means
  "nothing to inherit", so a tag under an opted-out context can re-enable with
  its own configs. Semantics are documented at mergeStructureTest().

Tests: 59 → 80, with new describes for the grouping and ordering, a
getCommands()/getCommandsByType() equivalence check over the fixture's cases,
the empty-render skip (new test/retag-manifest.yml fixture, kept separate so
the shared test/manifest.yml expectations in indexTest/executableTest stay
valid), and the opt-out at tag, context and global level. manifest.js branch
coverage 90.81% → 92.72%, statements/lines/functions still 100%; the remaining
uncovered branches are pre-existing defensive fallbacks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dave's commands were always independent within a type (builds of different
tags don't touch each other) but strictly ordered across types (nothing can
be tested before it is built). The executor now reflects exactly that:
main() walks manifest.getCommandsByType() with a barrier between types —
every command of a type finishes before the next type starts, and a type
with failures ends the run.

Within a type:

* -j 1 (default) keeps the old path bit-for-bit: runSerial wraps the
  untouched runCommand (spawn with inherited stdio), so output still streams
  live and a first failure aborts with the classic 'Command failed: <cmd>'
  error and exit 1.
* --jobs N runs a bounded worker pool (runPool): min(N, #commands) workers
  pull from a shared queue front-to-back, so commands start in the same
  deterministic sorted order the manifest produces. Each child runs through
  runCommandCaptured — spawn with piped stdio, stdout+stderr chunks buffered
  together in arrival order and replayed as ONE contiguous block on
  completion, bracketed by 'Running:' and 'Finished (Ns):' lines. Buffering
  is deliberately uncapped: exec()'s maxBuffer would kill a verbose child
  (the same pitfall runCommand already documents), and capturing is the
  whole point here, so we accumulate raw chunks ourselves. Concurrent
  children therefore never interleave a single line of output.
* On failure the pool stops handing out new work but never kills in-flight
  children; --keep-going/-k drains the rest of the type's queue instead
  (later types still never start). A single failure throws the classic
  error; several throw an 'N command(s) failed during <type>:' summary with
  one line and exit code per command.

--jobs validates via coerce (whole numbers >= 1 only) and argv parsing now
uses .fail(false) so bad options reject cleanly instead of yargs exiting
mid-parse; the entry-point handler already reports and exits 1.

Timing sanity (8 tags, build = sleep 1): 8.2s at -j 1 vs 2.1s at -j 4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Publishing shipped nearly the whole repo — most of all the 9.3 MB
decorative GIF under .images, plus tests and coverage output — because
.npmignore had gone stale: it still listed .eslintrc.yml and .travis.yml
and never covered anything added since. Replace it with a "files"
allowlist, which fails safe in the other direction: whatever lands in
the repo next stays out of the tarball unless it is listed. The package
goes from 9.5 MB across 43 files to 10.8 kB across 5.

Document --jobs/-j and --keep-going/-k in the README, together with the
two manifest-level opt-outs added earlier in this branch: a template
which renders to nothing skips the command, and structureTest: false
drops the configs which would otherwise trickle down. Add a CHANGELOG
whose first entry is 0.5.0, and bump the version to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
package.json and the README have always declared MIT, but the repo (and
therefore the published npm tarball) carried no license text. npm auto-
includes LICENSE regardless of the "files" allowlist, so 0.5.0 now ships it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aa8y
aa8y merged commit c7e2718 into master Aug 10, 2026
2 checks passed
@aa8y
aa8y deleted the parallel-execution branch August 10, 2026 23:09
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.

1 participant