APP := contra # What `carlos ship -target` defaults to, and therefore what a release # must be built for. Building for your own machine and shipping that is a # silent architecture mismatch: the binary uploads fine and fails to exec # on the instance. Override for a one-off: # make release RELEASE_GOARCH=amd64 RELEASE_GOOS ?= linux RELEASE_GOARCH ?= arm64 RELEASE_BIN := releases/$(APP)-$(RELEASE_GOOS)-$(RELEASE_GOARCH) # What the running binary reports from GET /api/version. The platform's # deploy verification polls that endpoint on every instance socket and # compares what it finds against the release it believes it adopted; an # unstamped binary answers "dev" from every build you have ever made, so # those two facts can never disagree and a process that was never # recycled onto the new release verifies green. Stamping this is what # gives that check the ability to fail. # # git describe, because the tag is what you ship against. Outside a # repository, or before the first commit, name it yourself: # make release VERSION=v0.1.0 VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null) .PHONY: build release version-check test test-js test-oracles test-webkit oracles vet fmt-check migration-check ci # build is the compile check: with more than one package matched, go # build discards the output, so this catches a broken package without # producing an artifact. build: CGO_ENABLED=0 go build ./... # release is what ships. -s -w drops the symbol table and DWARF, which on # this family's apps is 23-31% off the binary and 45-53% off the # compressed artifact — and compressed size is what actually gets # transferred. carlos is built the same way (see platform's Makefile). # # What survives: panic traces keep their function names AND line numbers # (Go's pclntab is untouched by these flags), debug.ReadBuildInfo works, # and go version -m still reports module metadata. What you lose is # source-level debugging with delve or gdb — build without the flags for # that. rastrillo dev never strips, for the same reason. release: version-check @mkdir -p releases CGO_ENABLED=0 GOOS=$(RELEASE_GOOS) GOARCH=$(RELEASE_GOARCH) \ go build -ldflags="-s -w -X 'github.com/carlosframework/rastrillo.BuildVersion=$(VERSION)'" \ -o $(RELEASE_BIN) ./cmd/$(APP) @echo @echo "built $(RELEASE_BIN) reporting version $(VERSION)" @echo "ship it with:" @echo " carlos ship -app $(APP) -target $(RELEASE_GOOS)-$(RELEASE_GOARCH) $(RELEASE_BIN)" # version-check refuses rather than letting release ship a binary whose # version is untrue. Both refusals are about the same thing: /api/version # is only evidence if the answer identifies one artifact. # # An empty stamp is worse than the "dev" default, because "dev" is # saying something true about itself and "" looks like a value that # arrived from somewhere. # # A dirty tree is refused rather than suffixed. A -dirty stamp names a # commit plus changes nobody can name, so two different binaries can # carry the identical string — which does not fix the check, it makes # it look reliable while it is not, and that is the more expensive # failure. Refusing costs one `git stash`; a wrong stamp ships. If you # mean to ship uncommitted work, say so out loud: # make release VERSION=v0.1.0-wip version-check: @if [ -z "$(VERSION)" ]; then \ echo "make release: no version to stamp."; \ echo " git describe found nothing here: no repository, or no commit yet."; \ echo " Commit, or name it: make release VERSION=v0.1.0"; \ exit 1; \ fi @case "$(VERSION)" in *-dirty) \ echo "make release: the tree is dirty, so $(VERSION) does not name what you are about to ship."; \ echo " Commit or stash first, or name it: make release VERSION=v0.1.0-wip"; \ exit 1;; \ esac test: go test ./... # The crypto twin. AGENTS.md: "Crypto is byte-identical in Go and JS, pinned # by shared golden vectors." This runs the browser modules against the same # vectors/contra-v1.json the Go suite uses, so a change to one side that is # not made to the other fails here rather than in somebody's vault. # # A HARD gate, and it does not soften when node is missing. A security product # must not silently stop verifying that the browser's crypto matches the # server's: a skip would be indistinguishable from a pass on the one check # that stops a Mac opening a vault the browser cannot. test-js: @command -v node >/dev/null 2>&1 || { \ echo "make test-js: node is not installed."; \ echo " This gate is not optional -- it is what proves the browser's"; \ echo " crypto matches the server's. Install node, do not skip it."; \ exit 1; } node --test test/crypto.test.mjs test/ecies.test.mjs test/items.test.mjs \ test/sharing.test.mjs # The third-party oracles: the QR symbols decoded by zxing-cpp and drawn # again by python-qrcode for a module-by-module comparison, and the two kit # PDFs read back by PyMuPDF and pypdf. # # These matter. Comparing matrices rather than only decoding is how the # reversed format-information bits were found — twelve wrong modules that a # decode-only check reports as "does not scan" and leaves you guessing. # # NOT part of `make ci`, and that is a deliberate, reluctant split made on # 2026-09-02 for the same reason test-webkit is out: the shared CI runner's # python3 is a bare 3.12.3 with no pip module and no ensurepip, so `make # oracles` cannot install anything there and the box is immutable-boot. A # gate that cannot run on the runner is a gate that is permanently red, and # permanently red CI teaches people to ignore red — which costs more than # this check is worth defending in the wrong place. # # It does NOT skip when the packages are missing; it fails with the command # that fixes it. Run it before trusting a change to qr.js, qrtables.js, # qrgf.js, pdf.js or kit.js: # # make oracles && make test-oracles # # Getting these back into CI is one line of python3-pip in the runner's # user-data. Filed rather than worked around. test-oracles: @command -v node >/dev/null 2>&1 || { \ echo "make test-oracles: node is not installed."; exit 1; } node --test test/qr.test.mjs test/kit.test.mjs # The third-party oracles' Python packages, for `make test-oracles`. # # Opt-in rather than a dependency of anything: a machine whose system python3 # already has them needs nothing, and test/oracle.mjs reaches for what this # builds only when it exists. Pinned in test/requirements.txt, because an # oracle that drifts is an oracle that stops meaning what it said last week. # # Two ways in, because a Debian python3 without the python3-venv package # cannot create a venv at all (no ensurepip) — which is exactly what the # shared runner turned out to be. A venv is preferred where it works; where # it does not, the packages go into .oracles/ and test/oracle.mjs puts that # on PYTHONPATH. Both give the oracles their imports without touching the # box, which is the requirement: its user-data is fixed at launch. oracles: @command -v python3 >/dev/null 2>&1 || { \ echo "make oracles: python3 is not installed."; exit 1; } @set -e; \ if python3 -m venv .venv >/dev/null 2>&1; then \ .venv/bin/pip install --quiet --upgrade pip; \ .venv/bin/pip install --quiet -r test/requirements.txt; \ echo "oracles ready: .venv/bin/python3"; \ elif python3 -m pip --version >/dev/null 2>&1; then \ rm -rf .venv; \ python3 -m pip install --quiet --target .oracles \ -r test/requirements.txt; \ echo "oracles ready: python3 with PYTHONPATH=.oracles"; \ else \ rm -rf .venv; \ echo "make oracles: this python3 can neither create a venv nor run pip."; \ echo " python3: $$(python3 --version 2>&1)"; \ echo " venv : $$(python3 -m venv /tmp/contra-venv-probe 2>&1 | tail -1)"; \ echo " pip : $$(python3 -m pip --version 2>&1 | tail -1)"; \ rm -rf /tmp/contra-venv-probe; \ echo " Install python3-venv or python3-pip, or set CONTRA_PYTHON to"; \ echo " an interpreter that already has test/requirements.txt."; \ exit 1; \ fi # The browser crypto in WebKit, which is the engine this product can least # afford to be wrong about and the one the Chromium rig never touches. # # NOT part of `make ci`, on purpose. It needs Playwright, which not every # machine will have, and a gate that cannot run everywhere is a gate that # teaches people to ignore red. It skips visibly rather than silently, and it # is what you run before trusting a change to the crypto modules. # # npm install playwright && make test-webkit test-webkit: node --test test/webkit.test.mjs vet: go vet ./... fmt-check: @out=$$(gofmt -l .); if [ -n "$$out" ]; then echo "gofmt needed:"; echo "$$out"; exit 1; fi # A step of its own, not folded into another target's recipe: ci.d/ # steps each exec one make target, so migration-check needs its own # name to get its own reported step on a runner with step support. # # go run, not a bare "rastrillo": go.mod already requires rastrillo, so # this needs nothing on PATH but the Go toolchain, and works offline # after go mod download. A bare "rastrillo migration check" failed a # fresh clone's very first CI run with "make: rastrillo: Command not # found", and nothing in the scaffold said what to install. migration-check: go run github.com/carlosframework/rastrillo/cmd/rastrillo migration check # ci is the one gate: what a runner executes and what you run before # pushing are the same definition. amadan's runner falls back to this # target when .amadan/ci is absent; ci.d/ carries the same targets, # reported one by one, for a runner with step support. migration-check # fails the build the moment models.go and migrations/ disagree, # instead of at boot on whatever machine notices next. If the app # declares manifest resources, also add: rastrillo generate --check ci: vet fmt-check test test-js migration-check