Development¶
Requirements¶
| For | Needs |
|---|---|
| build, unit tests | Go 1.25+ |
| integration, compat, golden | + docker, helm v4 on PATH |
| compat | + Helm 3 (make helm3 downloads it to .bin/helm3) |
| bench | + hyperfine, jq |
| docs site | Python 3 (make docs-serve creates .venv-docs/) |
Commands¶
make build # bin/forge, version via -ldflags "-X main.Version=$(VERSION)" (default 0.1.0)
make test # go test ./... — no network, no docker
go test -race ./... # what CI runs
go test ./internal/resolve -run TestName -v
make test-integration # registry:2 on localhost:5001, push fixtures, -tags integration
make compat # Helm 3 + Helm 4 compatibility gate
make golden # regenerate testdata/golden/* with real helm
make bench # hyperfine: helm vs forge cold/warm
make plugin-smoke # helm plugin install of this checkout, for helm + .bin/helm3
make registry-down # stop the local registry
make docs-serve # docs site with live reload, http://127.0.0.1:8000/forge/
make docs-build # mkdocs build --strict → site/
make plugin-smoke (scripts/plugin-smoke.sh) builds the host binary into a
fake release served on 127.0.0.1:18765 and points the install hook at it via
HELM_FORGE_PLUGIN_URL. Then, for each helm in $HELMS, it checks:
- a corrupted
checksums.txtmakes the install fail helm forge --versionandhelm forge cache pathwork- if the registry is up (
make fixtures),helm forge dep updategives the samecharts/andChart.lockas standalone forge
Helm 4's installer ignores HELM_PLUGINS, so the script isolates plugins with
HELM_DATA_HOME instead.
The local registry is plain HTTP — pass --plain-http when running forge
against it by hand:
Use a throwaway cache so you don't pollute ~/.cache/helm-forge:
Test layers¶
Unit tests¶
No network. Registry behaviour comes from internal/testutil/fakeregistry,
an in-memory OCI registry that records every request and can inject faults
(429, 5xx, digest mismatch, …). Chart repositories come from
internal/testutil/fakerepo: an index.yaml plus archives, with a request
log (marking requests that carried credentials), basic auth and queued error
statuses. internal/testutil.WriteChartTgz builds a
real chart archive for store/materialize tests.
Integration tests — test/integration/¶
Build tag integration. They run forge against the local registry
(localhost:5001) and chart repository (localhost:5002) through counting
reverse proxies, with no repositories.yaml, and assert:
- cold
dep buildmakes at most2 × unique OCI charts + 1 × unique chart-repository charts + 1 per sourcerequests; - the
-o jsonreport'sregistries[].requestsadd up to exactly what the proxies counted; - warm
dep buildmakes zero requests, reports no hosts, and every dependency iscached(localforfile://); charts/has one entry per unique chart;- after
cache gcempties the cache,dep buildis cold again and within the cold budget; dep tree/dep whymake zero requests, anddep outdatedat most onetags/listper OCI repository (+ one auth round per registry) and oneindex.yamlper chart repository.
If you change fetch or auth ordering and these fail, the request budget regressed — see Architecture → Request budget.
Compatibility gate — scripts/compat.sh¶
For every fixture × {update, build} × {Helm 4, Helm 3}, runs Helm and forge
on separate copies and diffs:
charts/listing- sha256 of every downloaded archive; for archives packaged from
file://directories, a hash of member names and contents (gzip bytes depend on the Go version each binary was built with, and Helm 3 stamps tar entries with the current time) Chart.lockwithoutgenerated:helm templateoutput
Fixtures using the chart repository get helm repo add fixtures … in Helm's
isolated config first, because helm dependency build refuses unregistered
repositories; forge runs with an empty config.
Override binaries with FORGE=… and HELMS="helm /path/to/helm3".
Benchmark — scripts/bench.sh¶
40 dependencies (bench-00 … bench-39) behind toxiproxy adding 50 ms to
every registry response. Compares helm dependency build, cold forge and warm
forge with hyperfine; results go to bench/results.md.
Targets (the script fails if missed):
- cold forge ≥ 5× faster than helm
- warm forge < 1 s
RUNS=10 make bench changes the number of runs.
The README's figure is a separate, real-world measurement: a parent chart with
50 subcharts against a remote registry, forge with 14 of them already cached,
helm with --skip-refresh:

Fixtures¶
| Path | Contents |
|---|---|
testdata/src/ |
Dependency charts pushed by scripts/fixtures.sh: dep-a (1.0.0, 1.1.0, 1.2.0, 2.0.0), dep-b (0.1.0, 0.2.0-rc.1), dep-c (1.0.0+build.1), plus 40 generated bench-NN. dep-a and dep-b are also served from a chart repository (.chartrepo/, nginx on localhost:5002) |
testdata/charts/<fixture>/ |
Parent charts, one feature each: alias, build-metadata, condition-tags, exact, file-local (chart in local-dep/), http-mixed, http-ranges, nodeps, prerelease, ranges, stale |
testdata/seeds/<fixture>/ |
Pre-existing charts/ contents copied in before a run (e.g. stale) |
testdata/golden/<fixture>/ |
Helm-generated Chart.yaml + Chart.lock |
internal/cli/testdata/json/ |
-o json output goldens (durations, paths and placement normalised). Regenerate with go test ./internal/cli -update and review the diff |
Adding a fixture¶
- Create
testdata/charts/<name>/Chart.yamlpointing atoci://localhost:5001/chartsorhttp://localhost:5002. Afile://dependency must live inside the fixture directory (not incharts/), because the scripts copy one fixture directory at a time. Helm cannot take one chart name from both an OCI registry and a chart repository. - If it needs new dependency versions, add them to
scripts/fixtures.sh. - If it needs pre-existing
charts/content, put it intestdata/seeds/<name>/. - Run
make golden— real Helm writestestdata/golden/<name>/. - Run
make compat— forge must match. - Add it to the fixture list in
test/integration/e2e_test.goif it should be covered by request-count checks.
Goldens are Helm's output. Never edit them by hand; if forge disagrees, forge is wrong.
CI¶
.github/workflows/ci.yml runs on pushes and PRs to main, manually
(workflow_dispatch), and as the first stage of a release (workflow_call):
| Job | Runs on | Does |
|---|---|---|
unit |
ubuntu, macos | go test -race ./... via gotestsum → JUnit test report; on ubuntu also cross-compiles for linux/darwin/windows × amd64/arm64 |
integration |
ubuntu | make fixtures, integration tests → JUnit test report, make compat → summary table, make plugin-smoke |
plugin |
ubuntu, macos, windows × Helm 3.22 / 4.1 | scripts/plugin-smoke.sh: install this checkout as a Helm plugin, checksum rejection, helm forge --version |
summary |
ubuntu | Job results in the run summary; fails if any job failed |
.github/workflows/docs.yml builds the docs site (MkDocs Material, config in
mkdocs.yml, pinned in docs/requirements.txt) with mkdocs build --strict
on PRs that touch docs/ or mkdocs.yml, and on main deploys it to GitHub
Pages (https://mmpyro.github.io/forge/). The pages in docs/ stay readable
on github.com; site-only extras (tabs, annotated code, mermaid clicks) degrade
to plain Markdown there.
Releasing¶
.github/workflows/release.yml runs when a v*.*.* tag is pushed:
plugin-version—plugin.yaml'sversionmust equal the tag (withoutv);ci— the full CI workflow above. Nothing is built if either fails.build— matrix on ubuntu, macos and windows runners:CGO_ENABLED=0binaries for linux/darwin/windows × amd64/arm64, version set to the tag. The runner-native binary is smoke-tested (forge --versionmust print the tag).release—checksums.txt, release notes from commits since the previous tag, GitHub release with all binaries attached. Tags containing-(e.g.v1.0.0-rc.1) become prereleases. The run summary lists files, sizes, sha256 and changes.plugin-smoke— on ubuntu, macos and windows × Helm 3 / Helm 4:helm plugin install https://github.com/mmpyro/forge --version <tag>(--verify=falseon Helm 4), thenhelm forge --versionmust print the tag.
To cut a release:
- Set
version:inplugin.yamlto the new version (nov), update the version in the install commands and download links inREADME.md,docs/index.mdanddocs/usage.md, and merge that tomain. - Tag and push:
Asset names (forge-<os>-<arch>, .exe on Windows) are what the download
links in README.md and the plugin install hooks
(scripts/install-plugin.sh, scripts/install-plugin.ps1) use. Keep them in sync.
Invariants to keep in mind¶
charts/changes only if every dependency succeeded.- Staging lives at
<chart>/.forge-staging/, never insidecharts/. - Store writes:
tmp/then rename. No locks. - Unsupported repositories (none, other schemes, unknown
@name, missingfile://directory) fail before any network call. - Exit codes:
0ok,1failure (engine.DependencyErrorlists each),2usage (cli.usageError). - Credentials only from Helm's registry config and
repositories.yaml.