Ship cross-platform release binaries - #14
Conversation
The repo has been a library so far: every subsystem is an interface seam with a tested reference implementation, and there is no server to start. That left nothing for a release to ship. This adds a small honest binary under cmd/openindex that reports the build version, the milestone plan, and the risk register, all read from the capacity package so the binary and the spec cannot drift. The version string is stamped at build time through -ldflags and defaults to dev for a plain go build. Output goes through an errWriter so every write is checked once at the end, which keeps the renderers readable and satisfies the linter.
The old release workflow predated the cmd binary and would have published empty archives. This rewrites it around the real binary, following the pattern the githome project uses for its releases. A version job resolves the build version once from the tag or git describe. A build matrix cross-compiles the binary for seventeen OS/arch pairs the pure-Go stack supports, with CGO disabled so the output is static, then verifies each binary's format with file(1) and packages it as a tar.gz or zip with a sha256 checksum. A smoke matrix builds and runs the binary natively on every target that has a GitHub-hosted runner, asserting the version flag echoes the build version and the plan lists the milestones, so we know the binaries start and not just that they link. A build-complete job gates on both matrices. On a semver tag the publish job attaches the archives and checksums to a GitHub release whose notes come from releases/<tag>.md.
|
|
||
| // render runs a write function against w and exits non-zero if the write fails, | ||
| // since a CLI that cannot emit its own output has nothing useful left to do. | ||
| func render(w io.Writer, fn func(io.Writer) error) { |
There was a problem hiding this comment.
render is the single choke point for output failure. If a write to stdout fails (a closed pipe, for instance) there is nothing sensible left for a reporter to do, so it exits non-zero rather than swallowing the error or panicking. Keeping that policy in one place is why every command in main funnels through it instead of checking errors inline.
| if ew.err != nil { | ||
| return ew.err | ||
| } | ||
| return tw.Flush() |
There was a problem hiding this comment.
The tabwriter Flush is deliberately the last thing and its error is returned. A tabwriter buffers every row until Flush computes the column widths, so an error surfaced before Flush would miss any write the flush itself does. Checking the accumulated errWriter error first, then returning Flush, covers both the row writes and the flush.
|
|
||
| // version is stamped at build time with -ldflags "-X main.version=...". A plain | ||
| // go build leaves it "dev". | ||
| var version = "dev" |
There was a problem hiding this comment.
version defaults to dev on purpose, and a test pins that default. A release build overrides it through -ldflags, and the smoke job in the release workflow asserts the running binary echoes the resolved build version. So the default is covered by a unit test and the stamped value is covered end to end on every runner-backed target.
|
Followed up on two things while writing this. First, I kept the binary strictly to what the library can answer truthfully today rather than stubbing out a serve command that would just error. A reporter that tells the truth about version, plan, and risks is more useful at this stage than a server shell that does nothing. Second, the seventeen-target matrix mirrors what the pure-Go stack actually supports; I built six representative targets locally (linux/arm, linux/riscv64, windows/arm64, darwin/amd64, freebsd/arm64, openbsd/amd64) before pushing so the full matrix should not surprise us. |
|
One note for when the production bindings land: the publish job reads release notes from releases/.md and falls back to generated notes if the file is absent, so cutting a release is write the notes, tag, push. The smoke assertions (version echo and plan listing the milestones) are intentionally cheap so they stay fast across five runners; as commands grow real behavior we can deepen them without slowing the gate much. |
What this does
OpenIndex has been a library up to this point. Every subsystem is an interface seam with a tested in-process reference implementation, and there is no long-running server to start yet. That left a release with nothing to ship. This change gives the project a real, working, version-stamped binary and a release workflow that builds it for every platform the pure-Go stack supports.
The binary
cmd/openindexis a small introspection CLI. It does not pretend to be the engine. It reports:version(and-version): the build version, stamped through-ldflags "-X main.version=...", defaulting todevfor a plaingo build.plan: the milestone build sequence M0 through M9 with its gates, read from thecapacitypackage.risks: the risk register, with the primary risk marked.Everything it prints comes from the
capacitypackage that landed in PR #13, so the binary and the spec cannot drift. Output is funnelled through anerrWriterso every write is checked once at the end (the Effective Go pattern), which keeps the renderers readable and satisfies the errcheck linter with no exclusions.The release workflow
.github/workflows/release.ymlis rebuilt around the real binary, following the pattern the githome project uses for releases:git describe.file(1)against its target (ELF, Mach-O, PE32), then packaged as a tar.gz or zip with a sha256 checksum.planlists the milestones. This proves the binaries start, not just that they link.releases/<tag>.md.Deliberate narrowing
The binary is an honest reporter, not the engine. It exposes only what the library can already answer truthfully today (version, plan, risks). The serving, crawling, indexing, and answer paths remain library seams and are not wired into a runnable command, because their production form is a later mechanical swap behind those seams and there is nothing to gain from a binary that claims capabilities it does not have. The release infrastructure is built now so that when those seams get their production bindings, shipping them is just a matter of growing the same command.
Verification
Locally:
gofmt -s -l,go vet,go test ./cmd/..., andgolangci-lint runare all clean. The binary runs and prints the version, plan, and risks. Six representative cross-compile targets were built to confirm the matrix is sound. No em-dashes or smart quotes in any of the new text.