Skip to content

Instantly share code, notes, and snippets.

@siavashs
Created September 1, 2026 15:23
Show Gist options
  • Select an option

  • Save siavashs/ff06eb8a0ff46df749be68329d68860d to your computer and use it in GitHub Desktop.

Select an option

Save siavashs/ff06eb8a0ff46df749be68329d68860d to your computer and use it in GitHub Desktop.
Make Alertmanager Great Again!?

Alertmanager Make Target Dependency Analysis

This report statically analyzes Alertmanager's Make targets and their dependency tree at upstream commit 7935b44682464fa7ba3e8a1f15a6f39eff1b3369.

Scope and method

The repository contains six Makefiles and one included common fragment:

No other .mk fragments were found. The root Makefile's only include is Makefile.common.

This is a static verification of declared prerequisites, recursive Make calls, recipes, documentation, and CI use. It does not include clean-tree executions, timestamp experiments, or parallel-build stress tests. Findings about stale outputs and races follow directly from the declared graph and should be confirmed dynamically before changes are merged.

Executive summary

The normal serial CI path is mostly coherent, but the Make graph has several correctness gaps:

  1. Root make test does not run the legacy Elm tests.
  2. The documented BINARIES=amtool option does not match the variable used by the build recipe.
  3. make clean && make build cannot restore all mandatory generated inputs.
  4. Legacy UI and email outputs can remain stale because their proxy targets omit real inputs.
  5. Elm test failures can be masked when the JUnit reporter is enabled.
  6. Several aggregates and multi-output generators are unsafe under make -j.
  7. The Mantine artifact is built but is not embedded in the Go binary or included in release artifacts.
  8. The mixin and TLS example subgraphs have missing dependencies and multi-output modeling problems.

Root dependency tree

The root Makefile includes Makefile.common before defining repository-specific targets. The common fragment supplies a generic %: common-% forwarding rule, while the root overrides build, test, and lint with explicit targets.

The first eligible non-pattern target is common-all, so bare root make uses this graph:

make / make all
└── common-all
    ├── precheck
    ├── style → common-style
    ├── check_license → common-check_license
    ├── lint
    │   ├── ui-elm → ui/app:build
    │   ├── ui-mantine-lint → ui/mantine-ui:lint
    │   └── common-lint
    ├── yamllint → common-yamllint
    ├── unused → common-unused
    ├── build
    │   ├── ui-elm → ui/app:build
    │   ├── ui-mantine → ui/mantine-ui:build
    │   └── common-build → promu → $(PROMU)
    └── test
        ├── ui-elm → ui/app:build
        ├── ui-mantine-test → ui/mantine-ui:test
        └── common-test

Sources: common-all, root build, test, and lint, and frontend wrappers.

GNU Make updates a shared target only once per invocation, so the three paths to ui-elm do not normally cause three builds in one root make run.

build-all

build-all
├── assets
│   ├── ui/app/src/Data
│   │   └── api/v2/openapi.yaml
│   ├── ui-elm
│   ├── ui-mantine
│   └── template/email.tmpl
├── apiv2
│   ├── api/v2/models
│   ├── api/v2/restapi
│   └── api/v2/client
└── build
    ├── ui-elm
    ├── ui-mantine
    └── common-build → promu → $(PROMU)

Source: root aggregate targets.

Other root targets

assets-tarball
└── ui-elm
    └── archive ui/app/dist under .tarballs/

fuzz-config
└── go test -fuzz=^Fuzz -fuzztime=5s ./config

clean
├── remove template/email.tmpl
├── remove api/v2/{models,restapi,client}
├── ui/app:clean
└── ui/mantine-ui:clean

Source: assets-tarball, fuzz-config, and clean.

Common aliases

The forwarding rule exposes these principal aliases:

all              → common-all
style            → common-style
check_license    → common-check_license
deps             → common-deps
test-short       → common-test-short
format           → common-format
vet              → common-vet
lint-fix         → common-lint-fix
yamllint         → common-yamllint
staticcheck      → common-staticcheck → root lint
unused           → common-unused
tarball          → common-tarball
docker*          → common-docker*
proto            → common-proto

The Docker aggregates fan out across amd64, armv7, arm64, ppc64le, and s390x, as selected before the common Makefile is included. See the root architecture override, generated architecture target names, and Docker target definitions.

Component dependency trees

Legacy Elm UI

The default target in ui/app is all:

all
├── src/Data
│   └── ../../api/v2/openapi.yaml
│       ├── generate src/Data/*.elm
│       ├── generate src/DateTime.elm
│       └── make format
├── build
│   └── dist/.build_stamp
│       ├── currently discovered src/**/*.elm
│       ├── index.html
│       ├── vite.config.mjs
│       └── package-lock.json
└── test
    └── node_modules
        └── package-lock.json

Sources: aggregate and dependency installation, tests, build stamp, and OpenAPI generation.

Other public targets are format, review, test, dev-server, build, and clean.

Mantine UI

The first target is node_modules, so bare make inside ui/mantine-ui only installs dependencies:

node_modules
├── package.json
└── package-lock.json

build
└── node_modules
    └── npm run build
        └── tsc && vite build

test
└── node_modules
    └── npm run vitest

lint
└── node_modules
    └── npm run check

Sources: Mantine Makefile and npm scripts.

The Make test target deliberately runs only Vitest; it does not invoke the package's composite npm test, which also checks and builds.

Email template generator

Bare make inside template only updates node_modules. Generation is explicit:

email.tmpl
├── email.html
├── inline-css.js
└── node_modules
    └── package-lock.json

Sources: template Makefile and generator inputs and output.

Alertmanager mixin

default
├── vendor
│   └── jb install
├── build
│   └── vendor
│       └── generate alertmanager_alerts.yaml
└── dashboards_out
    ├── mixin.libsonnet
    ├── config.libsonnet
    └── dashboards/*

Other targets are all, fmt, lint, and clean. None is declared phony. Source: mixin Makefile.

TLS HA example

gen-certs
├── certs/ca.pem
│   └── certs/ca-csr.json
├── certs/node1.pem
│   ├── certs/ca-config.json
│   ├── certs/ca.pem
│   ├── certs/ca-key.pem
│   └── certs/node1-csr.json
├── certs/node1-key.pem
│   └── same prerequisites as node1.pem
├── certs/node2.pem
│   └── corresponding CA and node2 inputs
└── certs/node2-key.pem
    └── same prerequisites as node2.pem

Source: TLS example Makefile.

Findings

High: root make test skips Elm tests

Root test depends on ui-elm, which recursively runs make build, not make test. It runs Mantine Vitest and Go tests, but not the Elm test suite. See root test and ui-elm.

The omitted Elm target performs formatting validation, elm-review, and elm-test: Elm test.

CI compensates by running make all separately inside ui/app, but local root make test does not provide the behavior described in AGENTS.md. See the frontend CI sequence.

High: documented single-binary builds use the wrong variable

The README recommends:

make build BINARIES=amtool

See README lines 43–46.

However, the build recipe passes $(PROMU_BINARIES) as Promu's positional binary list, and no local mapping from BINARIES to PROMU_BINARIES exists: common-build.

The effective invocation is:

make build PROMU_BINARIES=amtool

The same incorrect command appears in AGENTS.md.

High: make clean && make build cannot restore required inputs

Root clean deletes the generated email template, generated API packages, generated Elm sources, and UI build outputs: root clean and Elm clean.

Root build only builds the two UIs and invokes Promu; it does not regenerate the email template or API packages: root build.

The email template is a mandatory Go embed input: template/template.go.

Only serial make build-all restores all required generated inputs after clean. Either build should encode the necessary generation dependencies, or the restricted clean/build sequence should be documented explicitly.

High: legacy UI incremental builds omit real inputs

The Elm build stamp tracks only existing *.elm files, index.html, vite.config.mjs, and package-lock.json: build stamp rule.

It does not track:

  • src/main.js
  • src/assets/elm-datepicker.css
  • public/favicon.ico
  • elm.json
  • package.json
  • source-file deletions

src/main.js is the Vite entry and imports the CSS file, so either can change without invalidating dist/.build_stamp: src/main.js.

The Elm node_modules target also omits package.json: dependency rule.

High: parallel Make is unsafe

Several relationships rely on serial prerequisite visitation rather than actual dependency edges:

  • Root build, test, and lint place ui-elm beside Go compilation or package loading: root targets.
  • Go embeds ui/app/dist, so Go processing can begin before that directory exists or while it is being rebuilt: ui/web.go.
  • Root assets places src/Data generation beside ui-elm: assets.
  • Elm all makes generation, build, and tests siblings: Elm all.
  • build-all places assets, API generation, and binary building at the same level: build-all.
  • common-all permits unused, which mutates go.mod and go.sum, to overlap with lint, build, and tests under parallel execution: common-all and common-unused.

These aggregates should not be considered make -j safe.

High: API generation is modeled as three independent outputs

The API rule declares three normal targets that share one recipe:

api/v2/models api/v2/restapi api/v2/client: api/v2/openapi.yaml
	scripts/swagger.sh

See the root API rule.

The script removes and recreates all three directories, so parallel Make can launch multiple destructive generators against the same paths: scripts/swagger.sh.

The directory proxies also fail to detect an individual missing generated file while its directory remains. Inputs such as scripts/swagger.sh, COPYRIGHT.txt, and internal/tools/go.mod are not prerequisites.

A grouped target (&:) or one generation stamp would model the output set correctly.

High: Elm JUnit failures can be masked

When JUNIT_DIR is set, Elm tests run through:

elm-test --report=junit | tee ...

See Elm JUnit test recipe.

No pipefail behavior is configured, so the pipeline normally returns tee's status. A failing elm-test may therefore appear successful.

Medium: root email forwarding hides sub-Make prerequisites

The standalone template rule correctly depends on email.html, inline-css.js, and node_modules: template rule.

The root proxy depends only on email.html: root email rule.

If inline-css.js or package-lock.json changes while email.tmpl remains newer than email.html, root Make does not enter the sub-Make and the generated template remains stale.

Medium: generated Elm outputs are represented incompletely

The src/Data recipe also writes src/DateTime.elm, but only the directory is declared as a target: Elm generation rule.

If DateTime.elm or one generated Data file is removed while src/Data remains current, regeneration is not triggered. Generator image or Makefile changes also do not invalidate the output.

Medium: Mantine output is built but not consumed

Root build and assets both build the Mantine UI: root build and assets.

However:

This may be intentional during migration, but the target currently performs validation work rather than contributing to the production artifact.

Medium: mixin dependencies are incomplete

The mixin graph has several missing or misleading declarations:

  • vendor does not depend on jsonnetfile.json or jsonnetfile.lock.json.
  • dashboards_out consumes vendor and dashboards.jsonnet without depending on them.
  • Imported alerts.libsonnet and dashboards.libsonnet are omitted.
  • default places vendor and dashboards_out at the same level, creating a parallel race.
  • build writes alertmanager_alerts.yaml, but that output is not the target.
  • No command-like target is declared .PHONY.
  • clean leaves vendor and dashboards_out.

See the mixin Makefile and Jsonnet import graph.

The README also omits the required jb tool even though every build invokes jb install: mixin setup documentation.

Medium: TLS certificate generation has multi-output races

Each certificate recipe writes both a certificate and a key, but the files are declared as independent targets: TLS certificate rules.

make -j gen-certs can therefore run duplicate commands against the same output paths. These should use grouped targets or stamps.

The README installs cfssl but not the separately invoked cfssljson, and it does not document gen-certs: TLS setup documentation.

Medium: configured tool versions are not fully enforced

Promu and golangci-lint use unversioned binary paths. If a binary already exists at the path, Make does not verify that it matches the configured version. See tool version variables, Promu installation, and golangci-lint installation.

The active golangci-lint version override is in the root Makefile, despite the agent guide saying it is pinned in Makefile.common: root override.

Low: Docker and release-stage targets depend on external state

The Docker build targets expect prebuilt binaries under .build/${OS}-${ARCH} but do not depend on a cross-build target: Dockerfile inputs and Docker Make targets.

Similarly, publish, tag, and manifest targets do not depend on the preceding release stages. This may be intentional pipeline decomposition, but these targets are not self-contained.

Low: precheck currently does nothing

common-all depends on precheck, but the target has no prerequisites. PRECHECK_COMMAND_template is defined but not instantiated in this repository: precheck infrastructure.

Documentation accuracy

Documented target Assessment
make build Builds both UIs and both Go binaries; the guide mentions only Elm assets.
make build-all Accurate for serial execution, but not parallel-safe.
make build BINARIES=amtool Incorrect variable; the recipe uses PROMU_BINARIES.
make assets Incomplete description: it also builds both UIs and only incrementally regenerates file targets.
make apiv2 Correct intent, but output tracking and parallel behavior are fragile.
make test Incorrect: it skips the Elm tests.
make test-short Accurate: runs short Go tests only.
make lint Runs Go and Mantine checks, but only builds the Elm UI.
make common-format Accurate: runs Go formatting plus configured gofumpt/goimports formatters.
make fuzz-config Accurate.
make clean Accurate about removals, but make build cannot restore everything it removes.

The formatter configuration enables gofumpt and goimports: .golangci.yml.

Recommended remediation order

  1. Add a root ui-elm-test target and invoke it from root test.
  2. Correct the documented binary-selection variable or add a compatibility mapping from BINARIES to PROMU_BINARIES.
  3. Decide whether clean → build should be supported; encode generation dependencies or explicitly require clean → build-all.
  4. Complete the Elm build-stamp inputs and preserve Elm failures through the JUnit pipeline.
  5. Replace generated-directory proxies and independent multi-output rules with grouped targets or explicit stamp files.
  6. Add dependency edges that make root and UI aggregates safe under make -j.
  7. Expose all email generator inputs at the root level or make the root forwarding target phony.
  8. Decide whether Mantine is validation-only or part of the shipped artifact, then encode that decision in targets and documentation.
  9. Repair the mixin graph and document its actual tool requirements.
  10. Use versioned tool paths or stamps if configured tool versions must be enforced.

Suggested dynamic verification

Before applying fixes, confirm the static findings in a disposable worktree or clean checkout:

# Inspect expanded targets and recipes.
make -pn
make -n build-all

# Verify clean-build behavior.
make clean
make build

# Verify complete serial regeneration.
make clean
make build-all

git diff --exit-code

# Exercise parallel-sensitive paths.
make clean
make -j8 build-all
make -j8 apiv2
make -C ui/app clean
make -C ui/app -j8 all
make -C examples/ha/tls -j8 gen-certs

# Check direct target semantics.
make test
make lint
make build PROMU_BINARIES=amtool

Destructive clean commands should be run only in a disposable checkout or after preserving local work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment