Build every service from trunk
A complete set of binaries built from the mainline of every repository in your fleet, laid out by version and architecture, with repositories you do not want built opted out by a column in the list rather than by editing the flow.
When you finish you will have an artefact tree that a release flow can consume without knowing anything about how it was produced.
Why bother
Section titled “Why bother”Building thirty services by hand is not hard, it is just unrepeatable.
| Without this | With this |
|---|---|
| Build order and flags live in someone’s shell history | They live in a file with a diff history |
| Every developer’s binary is built slightly differently | One matrix — version, architecture, flags — applied to all |
| A service that should not ship gets built and shipped | A FALSE in the list, visible to everyone |
| Artefacts land wherever the person was standing | A predictable tree, so release automation needs no arguments |
| ”Which commit is this binary?” | The tree is versioned and the flow records it |
The flag matrix is the hidden value. ldflags, target architecture and version being set once means
every binary in the tree was built the same way — which is what makes the tree trustworthy as a
release input.
Before you start
Section titled “Before you start”| You need | Why |
|---|---|
| The fleet on disk | See Clone or refresh a repository fleet |
| A prepared build machine | See Prepare a build machine |
| The same CSV list | Reused, with its build column now doing work |
Step 1 — declare the build matrix
Section titled “Step 1 — declare the build matrix”Everything that makes a binary different from another binary goes here, and nowhere else.
name: build-trunkworkingdirectory: ~/workspacecontinueonerror: truevars: version: "1.2.0" osarch: linux-amd64 ldflags: "-s -w" only: ""tables: repos: type: csv file: ./fleet.csvonly is an escape hatch: empty means everything, otherwise a name to build just one. It costs one
variable and saves you writing a second flow.
Step 2 — lay out the artefact tree first
Section titled “Step 2 — lay out the artefact tree first”Create the destination before anything writes to it, so a build never fails on a missing directory halfway through.
tasks: - name: prepare-tree shell: | mkdir -p build/{{version}}/{{osarch}}Versioning the tree means two builds can coexist, which is what makes a rollback a copy rather than a rebuild.
Step 3 — say out loud what you are skipping
Section titled “Step 3 — say out loud what you are skipping”A silent skip and a silent failure look identical in a log.
- name: skip table: repos check: | "{{build}}" === "FALSE" shell: | echo "not building {{name}} (build=FALSE)"Step 4 — build the rest
Section titled “Step 4 — build the rest” - name: build table: repos check: | "{{build}}" === "TRUE" && ("{{only}}" === "" || "{{only}}" === "{{name}}") shell: | set -e cd {{name}} echo "building {{name}} @ {{version}}" GOFLAGS=-trimpath go build -ldflags "{{ldflags}}" \ -o ../build/{{version}}/{{osarch}}/{{name}} ./...The check is the whole opt-in mechanism: a row builds only if it says TRUE, and only if it
matches only when only is set.
Step 5 — prove the tree is complete
Section titled “Step 5 — prove the tree is complete” - name: manifest shell: | cd build/{{version}}/{{osarch}} ls -1 > MANIFEST.txt echo "built $(wc -l < MANIFEST.txt) artefacts for {{version}}/{{osarch}}" sha256sum * > SHA256SUMS 2>/dev/null || trueThe checksums are what let a release flow verify it is shipping what this flow produced.
The finished thing
Section titled “The finished thing”name: build-trunklist: trueworkingdirectory: ~/workspacecontinueonerror: truevars: version: "1.2.0" osarch: linux-amd64 ldflags: "-s -w" only: ""tables: repos: type: csv file: ./fleet.csv
tasks: - name: prepare-tree shell: | mkdir -p build/{{version}}/{{osarch}}
- name: skip table: repos check: | "{{build}}" === "FALSE" shell: | echo "not building {{name}} (build=FALSE)"
- name: build table: repos check: | "{{build}}" === "TRUE" && ("{{only}}" === "" || "{{only}}" === "{{name}}") shell: | set -e cd {{name}} echo "building {{name}} @ {{version}}" GOFLAGS=-trimpath go build -ldflags "{{ldflags}}" \ -o ../build/{{version}}/{{osarch}}/{{name}} ./...
- name: manifest shell: | cd build/{{version}}/{{osarch}} ls -1 > MANIFEST.txt echo "built $(wc -l < MANIFEST.txt) artefacts for {{version}}/{{osarch}}" sha256sum * > SHA256SUMS 2>/dev/null || truefunction main() { const repos = csv.read({ path: './fleet.csv' }).records; const built = [], failed = [], skipped = [];
for (const repo of repos) { if (only && repo.name !== only) { skipped.push(repo.name); continue; }
const out = `./dist/${osarch}/${repo.name}`; const r = shell.execute({ script: `go build -ldflags "${ldflags} -X main.version=${version}" -o ${out} ./cmd/${repo.name}`, workingDir: `~/workspace/${repo.name}`, capture: true, });
if (r.success) { built.push(repo.name); log.info(`built ${repo.name}`); } else { failed.push({ repo: repo.name, error: r.error }); log.error(`FAILED ${repo.name}: ${r.error}`); } }
// the report is the deliverable — a build that hides failures is worse than one that stops log.info(`built ${built.length}, failed ${failed.length}, skipped ${skipped.length}`); if (failed.length) throw new Error(`${failed.length} repositories failed to build`); return { built, skipped };}kis script run build-trunk.js --vars version=1.2.0 --vars osarch=linux-amd64Note the throw at the end rather than in the loop: every repository is attempted, and the run
still fails. That is what continueonerror: true plus a final check buys you in the flow.
Reach for the flow when the fleet is more than a handful. It records which repositories completed, so a rerun after fixing one broken repository does not rebuild the other thirty.
kis flow -f build-trunk.yamlBuild one service while iterating:
kis flow -f build-trunk.yaml -v only=service-alphaCut a different version without editing:
kis flow -f build-trunk.yaml -v version=1.2.1Verify
Section titled “Verify”MANIFEST.txt should list what you expect. Compare its line count against the number of TRUE rows
in the list — a mismatch means a build failed and continueonerror let the run finish, which is the
behaviour you want but only if you check.
Adapt it
Section titled “Adapt it”| Change | Where |
|---|---|
| Cross-compilation | Add GOOS/GOARCH from osarch in the build task |
| Non-Go services | A second table and a second build task; the tree layout is unchanged |
| Much faster | See Build a fleet in parallel |
| Reproducible from tags | See Build a tagged release (planned) |
Related
Section titled “Related”- Build a fleet in parallel
- Publish artefacts to object storage (planned)