Skip to the content.

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 .tarnot 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-localrepurposed, 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 implementedJenkinsfile/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 implementedJenkinsfile/ci-local carry the same echo-placeholder; no GitHub Actions workflow here either, same deliberate divergence as the npm/Python columns (unverified)

Notes

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

https://adr.github.io/

Architecture