Skip to content

Ship cross-platform release binaries - #14

Merged
tamnd merged 2 commits into
mainfrom
release-binaries
Jun 7, 2026
Merged

Ship cross-platform release binaries#14
tamnd merged 2 commits into
mainfrom
release-binaries

Conversation

@tamnd

@tamnd tamnd commented Jun 7, 2026

Copy link
Copy Markdown
Owner

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/openindex is 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 to dev for a plain go build.
  • plan: the milestone build sequence M0 through M9 with its gates, read from the capacity package.
  • risks: the risk register, with the primary risk marked.

Everything it prints comes from the capacity package that landed in PR #13, so the binary and the spec cannot drift. Output is funnelled through an errWriter so 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.yml is rebuilt around the real binary, following the pattern the githome project uses for releases:

  • version: resolves the build version once from the git tag or git describe.
  • build: a seventeen-target matrix (linux amd64/arm64/arm/386/ppc64le/s390x/riscv64/loong64, darwin amd64/arm64, windows amd64/arm64/386, freebsd amd64/arm64, openbsd amd64/arm64). CGO is disabled so the binaries are static and cross-compile cleanly. Each binary's format is verified with file(1) against its target (ELF, Mach-O, PE32), then packaged as a tar.gz or zip with a sha256 checksum.
  • smoke: builds and runs the binary natively on every target with a GitHub-hosted runner (ubuntu-24.04, ubuntu-24.04-arm, macos-15, windows-2025, windows-11-arm), asserting the version flag echoes the build version and plan lists the milestones. This proves the binaries start, not just that they link.
  • build-complete: gates on both matrices.
  • publish: on a semver tag, attaches the archives and checksums to a GitHub release whose notes come from 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/..., and golangci-lint run are 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.

tamnd added 2 commits June 7, 2026 18:53
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.
Comment thread cmd/openindex/main.go

// 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) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cmd/openindex/main.go
if ew.err != nil {
return ew.err
}
return tw.Flush()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cmd/openindex/main.go

// version is stamped at build time with -ldflags "-X main.version=...". A plain
// go build leaves it "dev".
var version = "dev"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@tamnd

tamnd commented Jun 7, 2026

Copy link
Copy Markdown
Owner Author

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.

@tamnd

tamnd commented Jun 7, 2026

Copy link
Copy Markdown
Owner Author

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.

@tamnd
tamnd merged commit e790e62 into main Jun 7, 2026
29 checks passed
@tamnd
tamnd deleted the release-binaries branch June 7, 2026 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant