Skip to content
Merged
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
83 changes: 83 additions & 0 deletions .claude/plans/2026-06-modernize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Modernization plan — June 2026

Branch: `aa8y/modernize/2026/6`.

Goals:
1. Get a working test runner on current Node.
2. Bump deps to current majors and migrate legacy patterns (callbacks → promises, CJS → ESM, Travis → GHA).
3. After upgrades, clear any residual audit findings.
4. Add tests for currently uncovered error paths — sparingly.
5. Keep each commit small, atomic, and green.

Each task below is one commit. Commit subject in backticks. Tick the box once committed.

---

- [x] **1. `chore(test): replace istanbul with c8 coverage runner`**
- Add `c8` devDep, remove `istanbul`.
- `npm test` becomes `c8 mocha 'test/**/*Test.js' && npm run lint`.
- Baseline reason: istanbul's CJS hook breaks on Node ≥ 22 because mocha's bundled yargs is now ESM. Nothing else can be verified until this lands.

- [x] **2. `chore(ci): switch from Travis to GitHub Actions`**
- Add `.github/workflows/ci.yml` (matrix on current Node LTSes, runs `npm ci && npm test`).
- Delete `.travis.yml`.
- Replace Travis build badge in README with GHA badge.

- [x] **3. `chore: drop legacy eslint config and bump .nvmrc`**
- Delete `.eslintrc.yml` (eslint 9 uses flat config exclusively).
- Set `.nvmrc` to current Node LTS.

- [x] **4. `chore(deps): patch/minor bumps within current majors`**
- Bump `async`, `js-yaml`, `lodash`, `mocha`, `eslint` to latest within their current major before tackling cross-major migrations.

- [x] **5. `refactor!: promise-based API; drop async package`**
- `lib/manifest.js`: `getMetadata` uses `fs/promises`; helpers stay sync.
- `lib/yargs.js`: parse via `yargs.parseAsync`.
- `index.js`: `main()` and `runCommand()` return promises; bin uses top-level await.
- Drop the `async` package — sequential execution becomes a `for…of` loop with `await`.
- Tests rewritten to async/await.
- Public API change is intentional (pre-1.0).

- [x] **6. `refactor!: migrate to ESM modules`**
- `package.json` gets `"type": "module"`.
- All `.js` files become `import`/`export`.
- `eslint.config.mjs` sourceType → `module`.
- Mocha needs no config change; `.mjs`/`.js` ESM works.
- **Reordered before yargs**: yargs 18 is ESM-only, so it can't be `require()`'d. ESM must land first.

- [x] **7. `chore(deps): upgrade yargs 13 → 18`**
- Update `lib/yargs.js` for the v18 API (factory `yargs()`, `parseAsync`).

- [x] **8. `chore(deps): upgrade chai 4 → 6`**
- chai 5+ is ESM-only — task 7 unblocks this.
- Update import + any drift in assertion syntax.

- [x] **9. `refactor: replace lodash with focused deep-merge helper`**
- Lodash is only used for `_.merge`. Swap in the `deepmerge` package (tiny, no transitive deps) or a hand-rolled merge. Drop full lodash dep.

- [x] **10. `chore(deps): clear residual audit findings`**
- At this point most vulns will already be gone (most came from istanbul + outdated direct deps).
- Run `npm audit fix` (and `--force` if a remaining advisory needs it) for whatever's left.
- Document any unresolvable findings in the commit message.

- [x] **11. `test: cover error paths in main(), manifest read, runCommand failure`**
- Add focused tests for the currently-uncovered branches:
- `main()` when the manifest path is missing.
- `getMetadata()` when YAML is malformed.
- `runCommand` rejection surfacing through `main()`.
- Keep it tight — one assertion per behaviour.

- [x] **12. `chore: bump version to 0.3.0; update README`**
- Bump `package.json` to `0.3.0`.
- Update README: remove the "pull/template not in 0.1.0" note (they're still not implemented; the framing is stale), refresh CI section, swap Travis badge.
- Final `npm audit` and `npm test` confirmation.

---

## Notes / decisions captured

- Public API: callbacks → promises is a breaking change; acceptable since pre-1.0.
- Lodash → `deepmerge`: `_.merge` mutates target and is recursive; `deepmerge` returns a new object. The codebase already passes `{}` as the target, so the behaviour is equivalent.
- ESM: mocha auto-detects ESM via `package.json` `type`. No `--experimental-vm-modules` needed on Node ≥ 22.
- The README mentions `pull` and `template` subcommands as "coming in 0.1.0 / 0.3.0" — both are still unimplemented. Out of scope for this branch; just remove the misleading future-tense lines in task 12.
- Audit fix moved to task 10 (per Arun): the cumulative upgrades clear most findings; the final `npm audit fix` is the residual sweep, not a chain reaction starter.
25 changes: 0 additions & 25 deletions .eslintrc.yml

This file was deleted.

28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches: [master]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test
- name: Upload coverage to Codecov
if: matrix.node == 24
uses: codecov/codecov-action@v5
with:
files: ./coverage/lcov.info
fail_ci_if_error: false
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22.3.0
24
13 changes: 0 additions & 13 deletions .travis.yml

This file was deleted.

37 changes: 37 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Dave

CLI tool that builds, tests, and pushes Docker images driven by a YAML manifest. The manifest declares contexts (Dockerfile directories) and tags per context; parameters/templates trickle from global → context → tag and are rendered with Mustache. ESM, async/await, public API returns promises.

## Entry points

- `index.js` — bin (`dave`). Parses argv, loads manifest, computes the full list of shell commands, and awaits them one at a time in a `for…of` loop. `runCommand` is `promisify(child_process.exec)` with logging around it.
- `lib/yargs.js` — argv parsing on top of yargs 18 (`yargs(args).command(...).parseAsync()`). Subcommands: `build | test | push | all`. Options: `--context/-c`, `--tags/-t` (string array — `string: true` keeps `1.0` from collapsing to a Number), `--manifest/-m` (default `./manifest.yml`). `all` expands to `['build','test','push']`. Commands are always reordered to `build → test → push`. If `--tags` is given without `--context`, context defaults to `'.'`.
- `lib/manifest.js` — manifest reader and command builder. Walks contexts (sorted) × tags (sorted) × types (`build|test|push`), merging parameters/templates with the inline `deepMerge` helper and rendering Mustache. `tagKeys` lets a tag name be reused as another parameter (e.g. `sparkVersion` = tag); `tag_keys` and `tag-keys` aliases are also accepted.

## Conventions worth knowing

- Templates use Mustache: `{{var}}` HTML-escapes, `{{{var}}}` doesn't. `repository` values contain `/` so they always use triple braces.
- Output ordering is deterministic: types in fixed order, contexts/tags lexicographic. Tests rely on this.
- `deepMerge` in `lib/manifest.js`: source-wins-on-overlap recursive merge over plain objects. Arrays and non-plain values overwrite. Null sources skip. Matches the `_.merge` semantics we relied on before; do not "fix" it to merge arrays index-wise without checking tests.
- `serialize-javascript` is pinned via npm `overrides` to clear an advisory mocha 10 hasn't picked up. Don't drop the override casually.
- Code style enforced by eslint flat config (`eslint.config.mjs`, ESM): 2-space indent, no semicolons, no space before function paren (but space allowed for `async () =>`), unix linebreaks.

## Commands

```
nvm use # picks Node from .nvmrc (24)
npm install
npm test # c8 + mocha + lint
npm run lint # eslint only
./index.js build --context . --tags foo --manifest ./test/manifest.yml
```

`npm test` runs `c8 --reporter=text --reporter=lcov mocha 'test/**/*Test.js' && npm run lint`. The lcov output is what CI uploads to Codecov.

## Test layout

- `test/indexTest.js` — exercises `main()` end-to-end against `test/manifest.yml` and `runCommand()` against real shell commands (`true`, `false`, `ls`, pipes). Also covers `main()` rejection when the manifest is missing.
- `test/lib/manifestTest.js` — extensive unit coverage of the trickle-down merge + Mustache rendering, plus `getMetadata` happy path and ENOENT / YAMLException failures. Expected-output arrays are the best reference for understanding ordering and inheritance.
- `test/lib/yargsTest.js` — argv parsing, command filtering/ordering, options defaults.
- `test/manifest.yml` — fixture with context `.` and tags `begin`, `end`, `wait`.
- `test/malformed.yml` — malformed YAML used by the `getMetadata` failure test.
62 changes: 32 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dave

[![Build Status](https://travis-ci.org/aa8y/dave.svg?branch=master)](https://travis-ci.org/aa8y/dave)
[![CI](https://github.com/aa8y/dave/actions/workflows/ci.yml/badge.svg)](https://github.com/aa8y/dave/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/aa8y/dave/branch/master/graph/badge.svg)](https://codecov.io/gh/aa8y/dave)

Dave is a tool which is intended to help with Docker image authoring. It tries to fill the gaps Docker Hub has around building images, which are:
Expand All @@ -16,16 +16,14 @@ Dave is a tool which is intended to help with Docker image authoring. It tries t

## Features

Dave would do perform its operations by using metadata in a [YAML](http://yaml.org/) serialized manifest file. The format is explained [later](#manifest_file). The following operations are supported.
Dave performs its operations by using metadata in a [YAML](http://yaml.org/) serialized manifest file. The format is explained [later](#manifest-file). The following operations are supported.

* **all**: This command will execute all commands except `template` in the order of `pull`, `build`, `test` and `push`.
* **all**: Executes `build`, `test` and `push` in order.
* **build**: Builds one or more images using the given Docker build command template and its arguments.
* **pull**: Pulls one or more images from a Docker registry like Docker Hub.
* **push**: Pushes a local image using the given Docker push command template and its arguments.
* **template**: Builds one or more `Dockerfile`s using the given template. This helps keep your `Dockerfile`s DRY.
* **test**: Tests a Docker image by invoking a certain command on the image. A non-zero exit status code fails the test.

All templating is done using [Mustache](https://mustache.github.io/). `pull` and `template` commands won't be supported in the first release which would be `0.1.0`.
All templating is done using [Mustache](https://mustache.github.io/).

## Usage

Expand Down Expand Up @@ -172,42 +170,46 @@ And while the manifest file can be named anything, the default name assumed is `

## Examples

Here are projects where Dave is being utilized to build, test and push images. See `manifest.yml` to see how the metadata has been stored and `.travis.yml` to see how Dave can be leveraged.
Here are projects where Dave is being utilized to build, test and push images. See `manifest.yml` to see how the metadata has been stored.

* [aa8y/docker-scala](https://github.com/aa8y/docker-scala): A simple Docker project with one `Dockerfile` (i.e. one context) from which all Docker images are built.

## CI Builds

### TravisCI
### GitHub Actions

Here's an example `.travis.yml` to use it with TravisCI.
Here's an example workflow to use Dave with GitHub Actions:

```
sudo: required
services:
- docker
language: node_js
node_js:
- stable
before_install:
- git clone https://github.com/aa8y/dave.git
install:
- npm install -g dave/
before_script:
- dave build
script:
- dave test
after_success:
- docker login -u <username> -p "$DOCKER_PASSWORD"
- dave push
```yaml
name: Docker images

on:
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm install -g dave
- run: dave build
- run: dave test
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: dave push
```

If all you want to do is build and test the images, you can ignore the `after_success` section. But if you do want to push the images after they have been tested, you would need a way to authenticate your Docker user. For that, follow [this guide](https://docs.travis-ci.com/user/docker/#Pushing-a-Docker-Image-to-a-Registry). I, personally, only like to encrypt my password as the username for Docker registry is usually also the namespace for the images. Also, I am working on acquiring the [Dave package namespace](https://www.npmjs.com/package/dave) on NPM, so that the installation process is easier.
Drop the login and `dave push` steps if you only want to build and test.

## Future Work

* Add support to pull images. This should help pulling cached layers which can make building images faster on a CI instance. I expect to add this in the 0.2.0 release.
* Add support for templating `Dockerfile`s. I expect to add this in the 0.3.0 release.
* Verify the metadata read from the manifest against a schema. Maybe use [JSON Schema](http://json-schema.org/)?

## License
Expand Down
10 changes: 7 additions & 3 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,20 @@ export default [...compat.extends("eslint:recommended"), {
...globals.node,
},

ecmaVersion: 6,
sourceType: "commonjs",
ecmaVersion: "latest",
sourceType: "module",
},

rules: {
indent: ["error", 2],
"linebreak-style": ["error", "unix"],
"no-console": [0],
semi: ["error", "never"],
"space-before-function-paren": ["error", "never"],
"space-before-function-paren": ["error", {
anonymous: "never",
named: "never",
asyncArrow: "always",
}],
"spaced-comment": ["error", "always"],
},
}];
Loading
Loading