Architecture Decision Record (ADR)
ADR-0045: Software build lifecycles Ultra-light Architecture Decision Record (ADR)
Use this when speed and simplicity are more important than detail.
1. Status:
Draft
2. Context
Different technology stacks (Java/Maven, Node.js, Python, Elixir, C++/CMake) have varying build lifecycles. To provide a consistent developer experience and standardized CI/CD pipelines, we need to map these diverse tools to a unified set of lifecycle phases. Maven’s build lifecycle serves as the primary reference model.
3. Decision
Standardize software build lifecycles across the organization by adopting the following mapping table as a reference
for CI/CD pipeline definitions and local development workflows. Where a tool does not natively support a phase name or
it is reserved, project-specific aliases (e.g., in package.json, mix.exs, Makefile, or pyproject.toml) must be
used to map the harmonized name to the underlying tool command:
TODO: Remove lines, those most likkelly are not relevant to other than maven. That means some things in other langiages are already done or are not relevant at all.
| Group | Phase / Lifecycle | Maven | Node.js (npm) | Python (venv + pip) | Elixir (mix) | CMake / Make |
|---|---|---|---|---|---|---|
| Workspace | Bootstrap | mvn dependency:go-offline |
npm run bootstrap (npm ci) |
python3 scripts/bootstrap.py (creates .venv, then a single pip install -r requirements.txt — the org standard: one requirements.txt per repo, listing every packages/* member as an -e editable entry alongside the pinned dev tooling; pip’s resolver handles the whole set in one pass regardless of listing order, verified against a deliberately reverse-ordered file, so no manual topological install loop — see setmy.info-python/report.md Round 2) |
mix deps.get — umbrella root, no separate isolation step needed; Mix deps live in the project’s own deps//_build/, not a global store (verified) |
make bootstrap |
| Workspace | Clean | mvn clean |
npm run clean |
.venv/bin/python scripts/run_workspaces.py clean (fans scripts/clean.py out per module, reversed order) |
mix clean (verified) |
make clean |
| Validation | Validate | mvn validate |
npm run validate |
.venv/bin/python scripts/run_workspaces.py validate (structural checks + mypy --strict for any module with its own [tool.mypy] table) |
mix validate (custom Mix.Tasks.Validate in apps/dev_tasks; structural checks + mix dialyzer for any app with its own :dialyzer project key — only demo_module_b here, verified) |
make validate |
| Validation | Format | mvn spotless:apply |
npm run format (prettier --write .) |
.venv/bin/python scripts/format.py (ruff format ., whole repo, one pass — not black; documented divergence from an earlier draft of this row, see setmy.info-python/report.md) |
mix format (verified) |
make format |
| Validation | Format Check | mvn spotless:check |
npm run format:check (prettier --check ., no writes) |
.venv/bin/python scripts/format_check.py (ruff format --check .) — verified --check and the write mode are genuinely separate code paths, unlike prettier’s flag-precedence gotcha |
mix format --check-formatted (verified — native flag, no custom task needed) |
make format-check (unverified) |
| Validation | Lint | mvn checkstyle:check |
npm run lint (ESLint over every workspace and tools/) |
.venv/bin/python scripts/lint.py (ruff check ., whole repo including scripts/ itself) |
mix credo --strict (Credo; .credo.exs generated via mix credo.gen.config; verified passes with zero issues) |
make lint |
| Generation | Generate Sources | mvn generate-sources |
Not implemented | Not implemented | Not implemented — same as npm/Python, no demo app needs generated source | make generate-sources |
| Generation | Process Resources | mvn process-resources |
npm run resources -- --profile <name> (tools/resources.js; ${propertyName} token substitution from profiles/<name>.json, ADR-0041/ADR-0042 canonical profile names, hard-validated) |
.venv/bin/python scripts/run_workspaces.py resources -- --profile <name> (scripts/resources.py; identical ${propertyName} substitution scheme and canonical-profile validation as the npm column — but YAML profiles, profiles/<name>.yaml read via PyYAML, not the npm column’s JSON: YAML is the org’s cross-language default profile format per requirements-rules.md §6.3, the npm implementation alone deliberately keeps JSON) |
mix resources --profile <name> (custom Mix.Tasks.Resources; identical ${propertyName} substitution scheme, but YAML profiles — profiles/<name>.yaml, not JSON, matching the Python column’s own YAML choice — verified) |
make process-resources |
| Build | Compile | mvn compile |
npm run build (esbuild bundle to dist/index.js + dist/index.min.js) |
.venv/bin/python scripts/run_workspaces.py build (scripts/build.py — plain Python has no transpile/bundle step, so this is an import-time smoke check + dist/build-info.json, a documented divergence, not a no-op silently dropped) |
mix compile --warnings-as-errors (verified, native — Erlang bytecode compile is a real build step here, unlike npm/Python’s own divergences on this row) |
cmake --build build |
| Build | Process Classes | mvn process-classes |
N/A — no separate compiled-artifact processing step for esbuild’s output | N/A — same reasoning as npm | N/A — no separate post-compile processing step for BEAM bytecode | make process-classes |
| Test | Generate Test Resources | mvn generate-test-resources |
Not implemented | Not implemented | Not implemented — same as npm/Python | make generate-test-resources |
| Test | Process Test Resources | mvn process-test-resources |
Not implemented | Not implemented | Not implemented — same as npm/Python | make process-test-resources |
| Test | Test Compile | mvn test-compile |
N/A — plain JS test files run directly via node --test, no compile step |
N/A — plain test_*.py files run directly via pytest, no compile step |
N/A — *_test.exs files compile automatically as part of mix test.unit/.integration/.e2e, no separate step (verified) |
make test-compile |
| Test | Unit Test | mvn test |
npm test (tooling self-tests + every workspace’s test/unit/) |
.venv/bin/python scripts/tooling_test.py (build tooling’s own scripts/test/{unit,integration}/, §7.7) and .venv/bin/python scripts/run_workspaces.py test (every module’s test/unit/, via pytest) — two explicit steps, not one combined command; Python has no per-directory script-name-scoping trick to fold them the way npm’s root/per-package test script does |
mix tooling_test (build tooling’s own apps/dev_tasks/test/{unit,integration}/, §7.7) and mix test.unit (every demo app’s test/unit/, root mix.exs alias scoping mix test to those four paths) — two explicit steps like npm; a bare mix test at the umbrella root would recurse into every app but not distinguish tiers (verified) |
ctest |
| Test | Pre Integration Test | mvn pre-integration-test |
npm run pre-integration-test (starts a real HTTP server for modules that declare config.server) |
.venv/bin/python scripts/run_workspaces.py pre-integration-test (scripts/pre_integration_test.py; loads an optional per-module pre_it.py hook, which starts scripts/http_server.py — stdlib http.server, no framework — on that module’s own [tool.server] port) |
mix pre_integration_test (custom Mix.Tasks.PreIntegrationTest; starts a real Plug.Cowboy instance per demo app via Mix.Tasks.Server, on that app’s own configured port from config/config.exs — verified) |
make pre-integration-test |
| Test | Integration Test | mvn integration-test |
npm run integration-test (runs test/integration/ against the built dist/index.js) |
.venv/bin/python scripts/run_workspaces.py integration-test (runs test/integration/ — directory-based per requirements-rules.md §7.1, not pytest -m integration markers; only exercises the public API re-exported from each module’s __init__.py, since plain Python has no meaningfully-different “built” artifact to import from instead) |
mix test.integration (root mix.exs alias, directory-based test/integration/ per app, public-API-only — same divergence rationale as Python; verified) |
make integration-test |
| Test | Post Integration Test | mvn post-integration-test |
npm run post-integration-test (stops the server; runs even if integration tests failed) |
.venv/bin/python scripts/run_workspaces.py post-integration-test (loads the module’s optional post_it.py hook, stops the server) |
mix post_integration_test (custom Mix.Tasks.PostIntegrationTest; stops every started server — verified, ci-local/lib.sh runs this unconditionally even if integration tests failed, mirroring Jenkinsfile’s post { always { ... } }) |
make post-integration-test |
| Test | Pre E2E Test | mvn -Pe2e pre-integration-test |
npm run pre-e2e-test |
.venv/bin/python scripts/run_workspaces.py pre-e2e-test (pre_e2e.py hook — same server start as pre-integration-test) |
mix pre_e2e_test (custom, same server-start mechanism as pre_integration_test — verified) |
make pre-e2e-test |
| Test | E2E Test | mvn -Pe2e integration-test |
npm run e2e-test (test/e2e/ against the built dist/index.min.js and, for module a, real HTTP requests against the running pre-e2e-test server) |
.venv/bin/python scripts/run_workspaces.py e2e-test (test/e2e/, directory-based; every module — not just one — has a test making a real HTTP request against the running pre-e2e-test server, §7.5) |
mix test.e2e (root mix.exs alias, directory-based test/e2e/; every demo app — not just one — has a test making a real HTTP request via :inets httpc against the running pre_e2e_test server, §7.5 — verified) |
make e2e-test |
| Test | Post E2E Test | mvn -Pe2e post-integration-test |
npm run post-e2e-test |
.venv/bin/python scripts/run_workspaces.py post-e2e-test (post_e2e.py hook, stops the server) |
mix post_e2e_test (custom, same cleanup mechanism as post_integration_test — verified) |
make post-e2e-test |
| Quality | Coverage | mvn jacoco:report |
npm run coverage (node --test --experimental-test-coverage; text report to console, HTML via npm run site) |
.venv/bin/python scripts/run_workspaces.py coverage (coverage run -m pytest per module, text report to console; HTML via the Site phase) |
mix coverage (root mix.exs alias wrapping mix coveralls, ExCoveralls, scoped to test/unit paths only — a bare mix coveralls auto-discovers every test tier including e2e and fails connection-refused unless every server happens to be running, hit this for real, not assumed — verified) |
make coverage |
| Quality | Security | mvn org.owasp:dependency-check:check |
npm audit |
.venv/bin/python scripts/security.py (pip-audit --skip-editable; root-level, not per-module — one shared .venv, not one environment per package the way npm workspaces’ nested node_modules gives you. --strict is deliberately not passed: it conflicts with --skip-editable, verified by hitting the conflict for real, not assumed) |
mix security (custom Mix.Tasks.Security; Sobelow run per demo app, not once at the root — Sobelow refuses to run at an umbrella root at all, confirmed directly — plus mix deps.audit --ignore-file .mix_audit_ignore at the root, one shared mix.lock. Two real cowlib CVEs found and deliberately, visibly ignored with full reasoning in .mix_audit_ignore per §12.3 — verified) |
make security |
| Quality | License | mvn license:check |
Not implemented | Not implemented | Not implemented — same as npm/Python | make license |
| Quality | Verify | mvn verify |
npm run verify (checks the expected build artifacts exist) |
.venv/bin/python scripts/run_workspaces.py verify (confirms dist/build-info.json — Build’s own output — exists and is well-formed, since Package hasn’t run yet at this point in the sequence) |
mix verify (custom Mix.Tasks.Verify; verified) |
make verify |
| Package | Package | mvn package |
npm run package (npm pack --pack-destination .artifacts/<module>) |
.venv/bin/python scripts/run_workspaces.py package (python -m build --outdir .artifacts/<dist-name>, sdist + wheel) |
mix package (custom Mix.Tasks.Package; mix hex.build per app into .artifacts/<dist-name>/ — but demo_module_c/demo_module_d are structurally skipped: Hex refuses to package any app with in_umbrella: true deps (“only Hex packages can be dependencies”), a real, unfixable-by-engineering constraint confirmed directly, not routed around — only demo_module_a/demo_module_b actually package — verified) |
cpack |
| Package | SBOM | mvn cyclonedx:makeAggregateBom |
npm run sbom (hand-rolled CycloneDX JSON per module, not a real CycloneDX generator) |
.venv/bin/python scripts/sbom.py (cyclonedx-py environment .venv — a real CycloneDX generator, one step more real than the npm column’s own placeholder; root-level, shared across all modules for the same reason as Security above) |
mix sbom (custom Mix.Tasks.Sbom; hand-rolled CycloneDX-shaped JSON placeholder like npm, not a real generator — none actively maintained exists for Hex/Mix, unlike Python’s cyclonedx-py; reads mix.lock via Mix.Dep.Lock.read/0, not Code.eval_file/1 — the latter produced spurious compiler warnings, verified — root-level, shared across the umbrella) |
make sbom |
| Package | Sign | mvn gpg:sign |
npm run sign (SHA-256 checksum per artifact — not a real signature yet) |
.venv/bin/python scripts/run_workspaces.py sign (SHA-256 checksum per packaged wheel/sdist — not a real signature yet, same acceptable-but-labeled placeholder as npm) |
mix sign (custom Mix.Tasks.Sign; SHA-256 checksum per packaged .tar — not a real signature yet, same placeholder as npm/Python — verified) |
make sign |
| Publish | Install | mvn install |
npm run install-local (custom, npm install --no-save <path> into the workspace root) |
.venv/bin/python scripts/run_workspaces.py install-local — repurposed, not the no-op-ish npm equivalent: installs the packaged wheel (+ every transitive local-dependency wheel, so modules with local deps resolve from local files) into a disposable throwaway venv and confirms it imports cleanly standalone, catching a packaging bug (missing package data) an editable install would mask |
mix install_local (custom Mix.Tasks.InstallLocal; repurposed like the Python column — Elixir doesn’t need this for umbrella siblings themselves (in_umbrella: true already links them at dev time), but installs the packaged .tar + every transitive in-umbrella-sibling .tar into a disposable scratch Mix project as path deps, catching packaging bugs a dev-time compile never exercises; shells out to system tar, not :erl_tar — the latter raised invalid_tar_checksum on a verified-uncorrupted Hex .tar, confirmed directly — verified) |
make install |
| Publish | Publish | mvn deploy |
npm run publish (resolves an npm dist-tag from the branch, real npm publish --ignore-scripts, always --dry-run unless PUBLISH_EXECUTE=true) |
.venv/bin/python scripts/run_workspaces.py publish (resolves a PEP 440 version suffix from the branch — master plain, release* → rcN, devel* → .devN, PyPI has no npm-style dist-tags — defaults to twine check, a validate-only no-op since twine has no native --dry-run; real twine upload only when PUBLISH_EXECUTE=true) |
mix publish (custom Mix.Tasks.Publish; never calls real mix hex.publish unless HEX_API_KEY is set — confirmed directly, not assumed, that mix hex.publish --dry-run --yes still prompts for Hex auth and hangs indefinitely with no TTY, unlike twine’s honest lack of a dry-run flag; dry-run path reuses mix hex.build instead. Publish-eligible branches: master/devel* only, not release* — matches the Jenkinsfile’s own when conditions exactly, a mismatch was caught and fixed here — verified. Only demo_module_a/demo_module_b are ever publishable, see Package above) |
make publish |
| Deploy | Deploy | mvn cargo:deploy (example) |
npm run deploy (writes a .deploy/<module>/<target>/deploy.json descriptor — prepared, not executed, no real target infrastructure yet) |
DEPLOY_TARGET=<target> .venv/bin/python scripts/run_workspaces.py deploy (writes a .deploy/<dist-name>/<target>/deploy.json descriptor — prepared, not executed, same as npm) |
DEPLOY_TARGET=<target> mix deploy (custom Mix.Tasks.Deploy; writes a .deploy/<dist-name>/<target>/deploy.json descriptor — prepared, not executed, same as npm/Python — verified) |
make deploy |
| Site | Site | mvn site:site |
npm run site (per-module + aggregated root site, linking every report below) |
.venv/bin/python scripts/run_workspaces.py site + .venv/bin/python scripts/aggregate_site.py (per-module + aggregated root site — verified, not sphinx-build) |
mix site (custom Mix.Tasks.Site; runs mix docs (ExDoc) then writes docs/reports/index.html linking lint/security/dependency-tree reports below — verified) |
(no standard equivalent) |
| Site | Docs | mvn javadoc:javadoc |
Bundled into npm run site (JSDoc HTML per module); a standalone npm run docs script also exists at the root for running just the docs step |
Bundled into the Site phase (scripts/docs.py, real pdoc — not stdlib pydoc — per module; verified, no longer sphinx-build/unverified) |
Bundled into the Site phase (mix docs, ExDoc, main: "readme", output: "docs" — one shared doc set for the whole umbrella, not per-app, since ExDoc is invoked once at the root — verified) |
doxygen (unverified) |
| Site | Dependency Tree | mvn dependency:tree |
Bundled into npm run site (npm ls --all --json; no standalone script) |
Bundled into the Site phase (pipdeptree --json-tree; root-level, shared across all modules, same reasoning as Security/SBOM above — verified, no longer unverified) |
Bundled into the Site phase (mix deps.tree output captured into the report; root-level, shared across the umbrella, same reasoning as Security/SBOM above — verified) |
(no standard equivalent) |
| Site | Site Deploy | mvn site:deploy |
Not implemented in Jenkinsfile (echo-placeholder — no Jenkins-native GitHub Pages push without picking real credentials). A GitHub Actions ci.yml previously did this for real (actions/deploy-pages, master branch only) but that workflow was deleted pending a deliberate rebuild — see report.md in setmy.info-js. |
Not implemented — Jenkinsfile/ci-local both carry the same echo-placeholder as the npm column, no GitHub Actions workflow was built for this repo at all (deliberately, given the npm side’s own experience above) |
Not implemented — Jenkinsfile/ci-local carry the same echo-placeholder; no GitHub Actions workflow here either, same deliberate divergence as the npm/Python columns |
(unverified) |
Notes
- Maven lifecycle is the reference model for all languages in the monorepo.
- Missing lifecycle phases in a language ecosystem SHOULD be implemented as custom scripts or build targets to preserve a consistent developer experience.
- Node.js should prefer
npm ciin CI andnpm installfor local development. - Python should prefer the built-in
venv+piptoolchain. Additional package managers (Poetry, PDM, uv, etc.) are implementation choices, not lifecycle requirements. - E2E testing is modelled as a specialization of the Integration Test lifecycle. In Maven, it is recommended to
execute E2E tests using a dedicated profile (e.g.
-Pe2e) and Failsafe includes patterns such as*E2ET.java, while still using the standardpre-integration-test→integration-test→post-integration-testphases. - SBOM generation should preferably use CycloneDX or another organization-approved standard.
4. Rationale (Justification):
Standardization reduces the cognitive load for developers switching between projects and simplifies the maintenance of shared CI/CD pipeline templates. By using Maven as the baseline, we leverage a well-understood and robust lifecycle model.
5. Consequences, Impacts & Follow-up Actions
- All new projects should aim to implement these scripts/commands.
- CI/CD pipeline templates will be updated to use these standardized phases.
- Documentation for each platform should reference this ADR for build instructions.
https://adr.github.io/